概述
ffmpeg对外提供了API接口,用户可以通过调用这些API来实现ffmpeg的各种功能。要设计video 基于ffmpeg的硬件解码框架,需要先深入了解ffmpeg的硬件解码框架。为此,本文先分析ffplay的代码框架,从ffplay入手,深入ffmpeg源码,分析ffplay如何调用NVIDIA硬件解码框架,在此基础上设计video 的硬件解码框架。本文只分析ffplay调用NVIDIA硬件解码框架,ffmpeg的硬件加速框架除了播放外,还在转码等方面使用,本文暂不分析ffmpeg转码部分的硬件加速框架。
1. 多媒体播放一般流程
2. ffplay框架分析
ffplay架构图
1一个主线程,主循环负责视频播放和SDL消息处理
2主线程中起一个解复用子线程,通过av_read_frame()接口将音视频分离开来,读取的视频包和音频包分别存入一个缓存队列video packets queue和audio packets queue。
3在解复用子线程中另起两个线程,分别为视频解码线程和音频解码线程,video queue 和audio queue中取出音视频包进行解码,将解码后的frame存入缓存队列 video frames queue和audio frames queue。
4 主线程通过获取video frames queue中的图像信息播放视频,ffplay默认以音频为主时钟,视频时钟向音频时钟同步。
ffplay框架如下所示。
ffplay主要结构体
1 struct VideoState
struct VideoState 结构是贯穿ffplay始终的最重要的一个结构体,它存储了程序所需的输入流信息,输出帧数据,时钟信息,解码前数据包缓存队列,解码后帧缓存队列,SDL控制信息,SDL窗口信息,视频参数信息,如视频高,宽等,音频采样信息,码率信息等,以及滤镜信息,视频图像缩放信息等等。
typedef struct VideoState {
SDL_Thread *read_tid; // demux解复用线程
AVInputFormat *iformat;
int abort_request;
int force_refresh;
int paused;
int last_paused;
int queue_attachments_req;
int seek_req; // 标识一次SEEK请求
int seek_flags; // SEEK标志,诸如AVSEEK_FLAG_BYTE等
int64_t seek_pos; // SEEK的目标位置(当前位置+增量)
int64_t seek_rel; // 本次SEEK的位置增量
int read_pause_return;
AVFormatContext *ic;
int realtime;
Clock audclk; // 音频时钟
Clock vidclk; // 视频时钟
Clock extclk; // 外部时钟
FrameQueue pictq; // 视频frame队列
FrameQueue subpq; // 字幕frame队列
FrameQueue sampq; // 音频frame队列
Decoder auddec; // 音频解码器
Decoder viddec; // 视频解码器
Decoder subdec; // 字幕解码器
int audio_stream; // 音频流索引
int av_sync_type;
double audio_clock; // 每个音频帧更新一下此值,以pts形式表示
int audio_clock_serial; // 播放序列,seek可改变此值
double audio_diff_cum; /* used for AV difference average computation */
double audio_diff_avg_coef;
double audio_diff_threshold;
int audio_diff_avg_count;
AVStream *audio_st; // 音频流
PacketQueue audioq; // 音频packet队列
int audio_hw_buf_size; // SDL音频缓冲区大小(单位字节)
uint8_t *audio_buf; // 指向待播放的一帧音频数据,指向的数据区将被拷入SDL音频缓冲区。若经过重采样则指向audio_buf1,否则指向frame中的音频
uint8_t *audio_buf1; // 音频重采样的输出缓冲区
unsigned int audio_buf_size; /* in bytes */ // 待播放的一帧音频数据(audio_buf指向)的大小
unsigned int audio_buf1_size; // 申请到的音频缓冲区audio_buf1的实际尺寸
int audio_buf_index; /* in bytes */ // 当前音频帧中已拷入SDL音频缓冲区的位置索引(指向第一个待拷贝字节)
int audio_write_buf_size; // 当前音频帧中尚未拷入SDL音频缓冲区的数据量,audio_buf_size = audio_buf_index + audio_write_buf_size
int audio_volume; // 音量
int muted; // 静音状态
struct AudioParams audio_src; // 音频frame的参数
#if CONFIG_AVFILTER
struct AudioParams audio_filter_src;
#endif
struct AudioParams audio_tgt; // SDL支持的音频参数,重采样转换:audio_src->audio_tgt
struct SwrContext *swr_ctx; // 音频重采样context
int frame_drops_early; // 丢弃视频packet计数
int frame_drops_late; // 丢弃视频frame计数
enum ShowMode {
SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
} show_mode;
int16_t sample_array[SAMPLE_ARRAY_SIZE];
int sample_array_index;
int last_i_start;
RDFTContext *rdft;
int rdft_bits;
FFTSample *rdft_data;
int xpos;
double last_vis_time;
SDL_Texture *vis_texture;
SDL_Texture *sub_texture;
SDL_Texture *vid_texture;
int subtitle_stream; // 字幕流索引
AVStream *subtitle_st; // 字幕流
PacketQueue subtitleq; // 字幕packet队列
double frame_timer; // 记录最后一帧播放的时刻
double frame_last_returned_time;
double frame_last_filter_delay;
int video_stream;
AVStream *video_st; // 视频流
PacketQueue videoq; // 视频队列
double max_frame_duration; // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
struct SwsContext *img_convert_ctx;
struct SwsContext *sub_convert_ctx;
int eof;
char *filename;
int width, height, xleft, ytop;
int step;
#if CONFIG_AVFILTER
int vfilter_idx;
AVFilterContext *in_video_filter; // the first filter in the video chain
AVFilterContext *out_video_filter; // the last filter in the video chain
AVFilterContext *in_audio_filter; // the first filter in the audio chain
AVFilterContext *out_audio_filter; // the last filter in the audio chain
AVFilterGraph *agraph; // audio filter graph
#endif
int last_video_stream, last_audio_stream, last_subtitle_stream;
SDL_cond *continue_read_thread;
} VideoState;
3. ffmpeg解码框架(API调用流程)
ffmpeg对外提供一套音视频处理的API。其中解码部分的接口主要的几个在下面列出。也是ffplay解码使用到的几个核心函数(不考虑线程,队列,SDL等处理),ffplay作为ffmpeg的一个播放器demo,为ffmpeg API的使用提供了很好的示例。
4. ffmpeg h264 NVIDIA硬件解码播放框架
在avcodec_send_packet 接口之前,软件解码和硬件解码的调用流程基本上是一样的。只有在查找解码器部分,软件解码调用的是 avcodec_find_decoder(codecid) ,而硬件解码调用的是 avcodec_find_decoder_by_name(“codec_name”)。
在解码函数中,即avcodec_receive_frame()中,根据是否启动了硬件加速选择软件解码还是硬件解码。
ffmpeg h264 NVIDIA硬件解码框架。ffmpeg为NVIDIA的h264硬件解码专门定义了一个文件cuviddec.c。在cuviddec.c中定义了NVIDIA硬件解码器,如下图倒数第二个框内所示。
简单提一下在解码函数里是怎么调到NVIDIA硬件解码器的。当调用
avcodec_find_decoder_by_name()获取到解码器后,会调用avcodec_open2()函数打开解码器,获取到的解码器就是在这个函数中赋值给avctx->codec的,
avctx->codec = codec;
这样,当解码器的 “receive_frame”字段不为空时,调用硬件解码:
if (avctx->codec->receive_frame)
ret = avctx->codec->receive_frame(avctx, frame);
else
ret = decode_simple_receive_frame(avctx, frame);
在cuviddec.c 中
#define DEFINE_CUVID_CODEC(x, X, bsf_name) \
static const AVClass x##_cuvid_class = { \
.class_name = #x "_cuvid", \
.item_name = av_default_item_name, \
.option = options, \
.version = LIBAVUTIL_VERSION_INT, \
}; \
const AVCodec ff_##x##_cuvid_decoder = { \
.name = #x "_cuvid", \
.long_name = NULL_IF_CONFIG_SMALL("Nvidia CUVID " #X " decoder"), \
.type = AVMEDIA_TYPE_VIDEO, \
.id = AV_CODEC_ID_##X, \
.priv_data_size = sizeof(CuvidContext), \
.priv_class = &x##_cuvid_class, \
.init = cuvid_decode_init, \
.close = cuvid_decode_end, \
.receive_frame = cuvid_output_frame, \
.flush = cuvid_flush, \
.bsfs = bsf_name, \
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AVOID_PROBING | AV_CODEC_CAP_HARDWARE, \
.caps_internal = FF_CODEC_CAP_SETS_FRAME_PROPS, \
.pix_fmts = (const enum AVPixelFormat[]){ AV_PIX_FMT_CUDA, \
AV_PIX_FMT_NV12, \
AV_PIX_FMT_P010, \
AV_PIX_FMT_P016, \
AV_PIX_FMT_NONE }, \
.hw_configs = cuvid_hw_configs, \
.wrapper_name = "cuvid", \
};
#if CONFIG_HEVC_CUVID_DECODER
DEFINE_CUVID_CODEC(hevc, HEVC, "hevc_mp4toannexb")
#endif
#if CONFIG_H264_CUVID_DECODER
DEFINE_CUVID_CODEC(h264, H264, "h264_mp4toannexb")
#endif
在 cuvid_decode_init, cuvid_decode_end, cuvid_output_frame, cuvid_flush, 4个回调函数中,调用了NVIDIA提供的库函数(API)。