ffplay.c是一个使用ffmpeg库的参考代码,实现了一个播放器的功能,源代码的的目录是:
ffmpeg-4.2.2/fftools/ffplay.c
ffplay.c可以作为入门来了解ffmpeg,我们也可以学习其编程方法实践到我们以后的开发中。本节和下一节对ffplay.c的源代码进行一个简单的描述,可以帮助读者来快速了解ffplay工具。另外ffplay.c的代码量还比较多,对于初学者而言,先了解其重要点,后面在做项目时慢慢深入即可。
假如本文媒体文件是由视频h264格式,音频aac格式,字幕封装而成。
1.我们从主函数开始分析,发现主要的功能函数如下:
int main(int argc, char **argv)
{
......
avdevice_register_all();
......
parse_options(NULL, argc, argv, options, opt_input_file);
......
if (SDL_Init (flags)) {
......
is = stream_open(input_filename, file_iformat);
......
event_loop(is);
return 0;
}
(1)注册编码解码器:avdevice_register_all();
(2)解析输入命令:parse_options();
用于解析用客输入的运行指令,了解用户意途
(3)初始化SDL,用于显示:SDL_Init ()
ffplay工具是用SDL控件来播放音视频
(4)打开媒体文件:stream_open()
这个函数是ffplay中最重要的函数,其作用是读一个媒体文件,然后将文件进行解封装,再然后进行解码。
(5)事件刷新(包括视频刷新): event_loop()
这个函数最主要的作用是将stream_open()得到的音视频帧进行播放。
至此,我们可以知道,播放器主要由两个函数完成工作,stream_open和event_loop
2.分析两个主要函数的工作内容:
2.1读文件函数stream_open():
(1)音视频媒体文件->解封装->视频编码文件(如h264)、音频(如aac)、字幕。
后面的处理视频、音频、字幕从原理上差不多,本节以视频为例子进行说明
(2)h264帧->存入packet队列。并创建一个用于解码h264帧的线程。
(3)从packet队列,取出一帧h264帧,用ffmpeg的api解码函数进行解码。h264->YUV数据。
(4)YUV帧->存入frame队列
2.2事件刷新函数event_loop():
(1).从frame中读取一个YUV帧
(2).SDL播放
(3)在播放过程中有音视频同步的策略协调
3.一步一步分析读文件函数
3.1 打开文件函数 stream_open()内容如下
static VideoState *stream_open(const char *filename, AVInputFormat *iformat)
{
......
if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
......
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
......
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
......
if (packet_queue_init(&is->videoq) < 0 ||
packet_queue_init(&is->audioq) < 0 ||
packet_queue_init(&is->subtitleq) < 0)
......
init_clock(&is->vidclk, &is->videoq.serial);
init_clock(&is->audclk, &is->audioq.serial);
init_clock(&is->extclk, &is->extclk.serial);
......
is->read_tid = SDL_CreateThread(read_thread, "read_thread", is);
......
}
(1)用于缓存功能的初始化
frame_queue_init()
packet_queue_init()
此两个函数,分别对应frame队列和packet队列,frame队列用于存放YUV数据,而packet队列用于存放如h264之类的数据
(2)同步时钟初始化
init_clock()
(3)读文件线程
read_thread
创建一个线程,专门用于读取媒体文件,并将媒体文件所包含的h264、aac,和字幕放在不同的packet队列,这步的过程叫解封装。
3.2read_thread内容
static int read_thread(void *arg)
{
.....
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
......
av_dump_format(ic, 0, is->filename, 0);
......
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
......
ret = av_read_frame(ic, pkt);
......
packet_queue_put(&is->videoq, pkt);
......
}
(1)//打开媒体
avformat_open_input();
(2)//输出媒体信息到控制台
av_dump_format();
(3)//打开视频/音频/字幕解码线程
stream_component_open();
(4)//获取一帧压缩编码的数据
av_read_frame();
(5)//将压缩编码的数据放入队列
packet_queue_put();
3.3 stream_component_open()内容
static int stream_component_open(VideoState *is, int stream_index)
{
......
codec = avcodec_find_decoder(avctx->codec_id);
......
if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
......
decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
......
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
......
SDL_PauseAudioDevice(audio_dev, 0);
......
decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
......
......
decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
......
}
(1)//获取解码器
avcodec_find_decoder();
(2)//开启音频、视频、字幕的解码线程
decoder_start()开启视频解码线程
3.4video_thread解码线程
static int video_thread(void *arg)
{
.......
for (;;) {
ret = get_video_frame(is, frame);
......
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
......
}
主要函数get_video_frame里面包含decoder_decode_frame函数
get_video_frame()->decoder_decode_frame()
3.5decoder_decode_frame内容
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
......
ret = avcodec_receive_frame(d->avctx, frame);
......
if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
return -1;
......
......
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {
......
}
avcodec_receive_frame 将成功的解码队列中取出1个frame
packet_queue_get获取队列数据
avcodec_send_packet发送数据到ffmpeg.放到解码队列中
至此,解码完成
3.6 queue_picture();
将3.4中get_video_frame函数所获得的YUV数据放入队列
frame_queue_push(&is->pictq);
4.事件刷新函数
4.1 event_loop()内容
static void event_loop(VideoState *cur_stream)
{
......
for (;;) {
.......
refresh_loop_wait_event(cur_stream, &event);
......
}
4.2 refresh_loop_wait_event()->video_refresh()->video_display()
其中video_refresh中包含video_display和音视频同步的逻辑
video_display内容如下
static void video_display(VideoState *is)
{
.......
video_open(is);
......
video_audio_display(is);
.......
video_image_display(is);
.......
}
//初始化
video_open();
//
video_audio_display();
//显示视频画面
video_image_display();
4.3 video_image_display的内容是从frame队列弹出一个YUV帧,并播放。内容如下
static void video_image_display(VideoState *is)
{
......
vp = frame_queue_peek_last(&is->pictq);
......
sp = frame_queue_peek(&is->subpq);
显示图像至SDL
......
}
}