Ffplay视频播放流程
2013-12-24 09:37
393 查看
背景说明
FFmpeg是一个开源,免费,跨平台的视频和音频流方案,它提供了一套完整的录制、转换以及流化音视频的解决方案。而ffplay是有ffmpeg官方提供的一个基于ffmpeg的简单播放器。学习ffplay对于播放器流程、ffmpeg的调用等等是一个非常好的例子。本文就是对ffplay的一个基本的流程剖析,很多细节内容还需要继续钻研。注:本文师基于ffmpeg-2.0版本进行分析,具体代码行还请对号入座,谢谢!
主框架流程
下图是一个使用“gcc+eygpt+graphviz+手工调整”生成的一个ffplay函数基本调用关系图,其中只保留了视频部分,去除了音频处理、字幕处理以及一些细节处理部分。注:图中的数字表示了播放中的一次基本调用流程,X?序号表示退出流程。
从上图中我们可以了解到以下几种信息:
三个线程:主流程用于视频图像显示和刷新、read_thread用于读取数据、video_thread用于解码处理;
视频数据处理:由read_thread读取原始数据解复用后,按照packet的方式放入到队列中;由video_thread从packet队列中读取packet解码后,按照picture的方式放入到队列中;由主流程从picture队列中依次取picture进行显示;
启动流程:启动流程如上图中的数字部分
退出流程:退出流程如上图中的X?序号部分
下面将对三个线程分别加以详细描述。
read_thread线程
从read_thread开始说起而不是从main线程,主要原因是考虑按照视频数据转换的方式比较好理解。read_thread的创建是在main-->stream_open函数中:
is->read_tid = SDL_CreateThread(read_thread, is); |
初始化部分:主要包括SDL_mutex信号量创建、AVFormatContext创建、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。对应ffplay.c文件中的2693-2810行代码;
循环读取数据部分:主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。对应ffplay.c文件中的2812-2946行代码;
反初始化部分:主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。对应ffplay.c文件中的2947-2972行代码;
初始化部分
主要包括SDL_mutex信号量创建、创建avformat上下文、打开输入文件、解析码流信息、查找音视频数据流并打开对应的数据流。创建wait_mutex互斥量
SDL_mutex *wait_mutex = SDL_CreateMutex(); |
//代码段一 /* if the queue are full, no need to read more */ if (infinite_buffer<1 && ……) { /* wait 10 ms */ SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); <-- line 2887 SDL_UnlockMutex(wait_mutex); continue; } //代码段二 ret = av_read_frame(ic, pkt); if (ret < 0) { if (ret == AVERROR_EOF || url_feof(ic->pb)) eof = 1; if (ic->pb && ic->pb->error) break; SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); <-- line 2925 SDL_UnlockMutex(wait_mutex); continue; } |
注:seek操作时(L1216)和音频队列为空(L2327)时,会发送continue_read_thread信号。
AVFormatContext创建
(AVFormatContext *)ic = avformat_alloc_context(); |
打开输入文件
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts); |
通过调用avformat_open_input函数,我们可以得到输入流的一个基本信息。我们可以通过调用av_dump_format(ic, 0, is->filename, 0);来输出解析后的码流信息,可以得到如下数据:
Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0 Duration: N/A, bitrate: N/A Program 1 Stream #0:0[0x68]:Video:h264 ([27][0][0][0] / 0x001B), 90k tbn Stream #0:1[0x67]:Audio:aac([15][0][0][0] / 0x000F), 0 channels |
² 封装格式是mpegts,包含两路数据流
² 流1的PID是0x68,类型是视频,编码格式是H264
² 流2的PID是0x67,类型是音频,编码格式是AAC
但是只有这些信息可定无法解码,比如视频的宽高比、图像编码格式(YUV or RGB …)、音频采样率、音频声道数量等等,以及Duration、bitrate等信息。这些信息都需要通过其他函数来解析。
解析码流信息
err = avformat_find_stream_info(ic, opts); |
解析码流的内部实现我们不在此处讨论,先看一看调用后该函数后解析出来的信息(同样采用av_dump_format来输出):
Input #0, mpegts, from '/home/nfer/bak/cw880-latency.ts':0B f=0/0 Duration: 00:02:53.73, start: 2051.276989, bitrate: 1983 kb/s Program 1 Stream #0:0[0x68]: Video: h264 (Baseline) ([27][0][0][0] / 0x001B), yuv420p, 1280x720, 30 tbr, 90k tbn, 180k tbc Stream #0:1[0x67]: Audio: aac ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp,72 kb/s |
² 码流信息;节目时长00:02:53.73,开始播放时间2051.276989,码率1983 kb/s
² 视频信息:色彩空间YUV420p,分辨率1280x720,帧率30,文件层的时间精度90k,视频层的时间精度180K
² 音频信息:采样率48000,立体声stereo,音频采样格式fltp(float, planar),音频比特率72 kb/s
需要注意的是,该函数是一个阻塞操作,即默认情况下会在该函数中阻塞5s。具体的实现是在avformat_open_input函数中有一个for(;;) 循环,其中的一个break条件如下:
if (t >= ic->max_analyze_duration) { av_log(ic, AV_LOG_VERBOSE, "max_analyze_duration %d reached at %"PRId64" microseconds\n", ic->max_analyze_duration, t); break; } |
{"analyzeduration", "specify how many microseconds are analyzed to probe the input", OFFSET(max_analyze_duration), AV_OPT_TYPE_INT, {.i64 = 5*AV_TIME_BASE }, 0, INT_MAX, D}, #define AV_TIME_BASE 1000000 <--file: avutil.h, line: 229 |
ic = avformat_alloc_context(); ic->interrupt_callback.callback = decode_interrupt_cb; ic->interrupt_callback.opaque = is; //add by Nfer ic->max_analyze_duration =1*1000*1000; av_log(NULL, AV_LOG_ERROR, "ic->max_analyze_duration %d.\n", ic->max_analyze_duration); err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts); |
查找音视频数据流
if (!video_disable) st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, wanted_stream[AVMEDIA_TYPE_VIDEO], -1, NULL, 0); |
// Find the first video stream videoStream=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { videoStream=i; break; } if(videoStream==-1) return -1; // Didn't find a video stream |
打开对应的数据流
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]); } |
查找解码器
avctx = ic->streams[stream_index]->codec; codec = avcodec_find_decoder(avctx->codec_id); |
设置解码参数
opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec); if (!av_dict_get(opts, "threads", NULL, 0)) av_dict_set(&opts, "threads", "auto", 0); if (avctx->lowres) av_dict_set(&opts, "lowres", av_asprintf("%d", avctx->lowres), AV_DICT_DONT_STRDUP_VAL); if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO) av_dict_set(&opts, "refcounted_frames", "1", 0); |
if (avcodec_open2(avctx, codec, &opts) < 0) return -1; |
packet_queue_start(&is->videoq); |
创建video_thread线程
is->video_stream = stream_index; is->video_st = ic->streams[stream_index]; is->video_tid = SDL_CreateThread(video_thread, is); is->queue_attachments_req = 1; |
循环读取数据部分
该部分是一个for (;;)循环,循环中主要包括pause和resume操作处理、seek操作处理、packet队列写入失败处理、读数据结束处理、然后是读数据并写入到对应的音视频队列中。for循环跳出条件
有两处是break处理的://代码段一 if (is->abort_request) break; <-- Line 2814 //代码段二 ret = av_read_frame(ic, pkt); if (ret < 0) { if (ic->pb && ic->pb->error) break; <-- Line 2923 } |
pause和resume操作处理
if (is->paused != is->last_paused) { is->last_paused = is->paused; if (is->paused) is->read_pause_return = av_read_pause(ic); else av_read_play(ic); } |
seek操作处理
if (is->seek_req) { ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags); if (is->video_stream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); } is->seek_req = 0; } |
同上面pause和resume的处理,is->seek_req是在按键操作(SDLK_PAGEUP、SDLK_PAGEDOWN、SDLK_LEFT、SDLK_RIGHT、SDLK_UP和SDLK_DOWN)时,调用stream_seek函数来修改is->seek_req标记变量,然后在read_thread线程中根据is->seek_req标记变量来进行处理。
具体处理除了调用ffmpeg的avformat_seek_file接口外,还向packet队列中放置了一个flush_pkt,这个在video_thread中的处理中会解决seek操作的花屏效果。
packet队列写入失败处理
/* if the queue are full, no need to read more */ if (infinite_buffer<1 && (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE || ( (is->audioq .nb_packets > MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request) && (is->videoq .nb_packets > MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request || (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) && (is->subtitleq.nb_packets > MIN_FRAMES || is->subtitle_stream < 0 || is->subtitleq.abort_request)))) { /* wait 10 ms */ SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } |
读数据结束处理
if (eof) { if (is->video_stream >= 0) { av_init_packet(pkt); pkt->data = NULL; pkt->size = 0; pkt->stream_index = is->video_stream; packet_queue_put(&is->videoq, pkt); } SDL_Delay(10); if (is->audioq.size + is->videoq.size + is->subtitleq.size == 0) { if (loop != 1 && (!loop || --loop)) { stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0); } else if (autoexit) { ret = AVERROR_EOF; goto fail; } } eof=0; continue; } |
向packet队列中放置一个null packet,此处用于loop时使用
判断是否是loop操作,如果是就seek到开始位置重新播放
如果是autoexit模式,就goto fail退出
注意,在读数据eof时,读数据部分还有些滞后,即if (is->audioq.size + is->videoq.size + is->subtitleq.size== 0)判断不一定为true,引起在判断前先delay了10ms(SDL_Delay(10););但是仍然不一定为true,因此需要continue。当然下一步av_read_frame失败也会返回AVERROR_EOF,eof会重新赋值为1。即,eof退出会wait到真正的播放完毕。
读数据并写入到对应的音视频队列
ret = av_read_frame(ic, pkt); if (pkt->stream_index == is->video_stream && pkt_in_play_range && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) { packet_queue_put(&is->videoq, pkt); } |
此处的处理实际上比较简单,就是av_read_frame和packet_queue_put,不详解。
反初始化部分
主要包括退出前的等待、关闭音视频流、关闭avformat、给主线程发送FF_QUIT_EVENT消息以及销毁SDL_mutex信号量。退出前的等待
/* wait until the end */ while (!is->abort_request) { SDL_Delay(100); } |
关闭音视频流
if (is->video_stream >= 0) stream_component_close(is, is->video_stream); |
其中stream_component_close关闭视频流做了以下处理:
终止packet队列:packet_queue_abort(&is->videoq);
发送信号给video_thread,避免继续解码阻塞:SDL_CondSignal(is->pictq_cond);
等待vide_thread线程退出:SDL_WaitThread(is->video_tid, NULL);
清空packet队列:packet_queue_flush(&is->videoq);
给主线程发送FF_QUIT_EVENT
if (ret != 0) { SDL_Event event; event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); } |
销毁SDL_mutex信号量
SDL_DestroyMutex(wait_mutex); |
video_thread线程
从主框架流程中可以看出,video_thread线程是在read_thread--> stream_component_open中创建的,负责从packet队列中读取packet并解码为picture,然后存储到picture队列中供主线程读取并刷新显示。video_thread的创建是在read_thread --> stream_component_open函数中:
is->video_tid = SDL_CreateThread(video_thread, is); |
初始化部分:主要包括AVFrame创建和AVFilterGraph创建。对应ffplay.c文件中的1881-1895行代码;
循环解码部分:主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。对应ffplay.c文件中的1897-1966行代码;
反初始化部分:主要包括刷新codec中的数据、释放AVFilterGraph、释放AVPacket以及释放AVFrame。对应ffplay.c文件中的1972-1978行代码;
初始化部分
该线程的初始化就是创建了AVFrame和AVFilterGraph,其中AVFilterGraph还是和编译宏包含,如果没有打开CONFIG_AVFILTER可以直接省略。is->video_tid = SDL_CreateThread(video_thread, is); … … AVFrame *frame = av_frame_alloc(); #if CONFIG_AVFILTER AVFilterGraph *graph = avfilter_graph_alloc(); #endif |
循环解码部分
主要包括pause和resume操作处理、读取packet处理、AVFILTER处理、然后是将picture写入视频队列中以及每次解码后的清理动作。pause和resume操作处理
video_thread中的关于pause和resume的处理比较简单,就是如果是pause状态就delay(线程sleep):while (is->paused && !is->videoq.abort_request) SDL_Delay(10); |
读取packet处理
avcodec_get_frame_defaults(frame); av_free_packet(&pkt); ret = get_video_frame(is, frame, &pkt, &serial); //关于frame的一些处理 av_frame_unref(frame); |
在ffmpeg-tutorial项目中tutorial01.c中的例子是使用avcodec_alloc_frame()来申请并设置default value的操作,但是在这里就分成了两步:av_frame_alloc()然后avcodec_get_frame_defaults(frame)。
av_free_packet实际上清空上一次get_video_frame中获取的packet数据,函数本身是有异常处理的,所以连续调用两次av_free_packet是没有问题的。
get_video_frame函数中主要部分是packet_queue_get然后avcodec_decode_video2,即从packet队列中读取数据然后进行解码,具体内容有机会另开文章进行讲解。
AVFILTER处理
AVFILTER处理是一个比较模块化很高的处理部分,大致流程包括以下几步:释放旧的AVFilterGraph并创建一个新的:avfilter_graph_free()和avfilter_graph_alloc()
配置video filters:configure_video_filters
向buffersrc中添加frame:av_buffersrc_add_frame
情况原有的frame和packet:av_frame_unref、avcodec_get_frame_defaults和av_free_packet
从buffersink中读取处理后的frame:av_buffersink_get_frame_flags
简单的理解就是:
将picture写入视频队列
如果需要avfilter处理,那么处理完后或者不需要avfilter处理,解码完成后的frame会调用queue_picture写入到picture队列中。具体细节不详解。解码后的清理动作
使用完packet后,必须从frame中释放出来:av_frame_unref。如api说明:Unreference allthe buffers referenced by frame and reset the frame fields.for循环跳出条件
有以下几种情况下会break出for循环:get_video_frame读数据失败,并且返回<0:该函数失败条件和read_thread其实是一致的,即当q->abort_request为true时;
configure_video_filters配置filter失败:该函数失败的情况下,我遇到的一种就是avfilter_graph_create_filter创建crop filter时失败,原因在于在configureffmpeg时没有把filter配置打开,导致只有默认的几个filter,其他一些特性filter都没有添加进行;
av_buffersrc_add_frame添加frame失败:该函数属于api,不详解;
queue_picture保存picture失败:该函数的失败条件是当is->videoq.abort_request为true时;
即正常情况下,有两种退出模式:
正常播放完成后退出,此时会通过get_video_frame读数据失败退出
如果是按ESCAPE和Q键退出,会直接退出,则不会等到,直接在queue_picture函数失败
反初始化部分
反初始化部分比较简单,就是先通知avcodec进行flush数据,然后依次释放AVFilterGraph、AVPacket和AVFrame。video_thread讲解的比较粗糙,主要原因还是由于个人了解的知识有所欠缺,后续有机会会补上。
主线程
主流程用于视频图像显示和刷新,实际上还主线程是一个事件驱动的,就是一个wait_event然后switch处理,然后继续for循环。refresh_loop_wait_event处理
该函数会从event队列中读取出event,SDL_PumpEvents、SDL_PeepEvents。同时会调用video_refresh来进行视频刷新和显示。此处会有大量和SDL API相关的操作,由于个人能力有限暂不分析。event的switch处理
该event的处理分为以下几类:SDL_KEYDOWN键盘按键事件
SDL_VIDEOEXPOSE屏幕重画事件
SDL_MOUSEBUTTONDOWN鼠标按下事件,如果启动ffplay时有exitonmousedown参数,会相应鼠标按下事件,然后退出播放;
SDL_MOUSEMOTION鼠标移动事件,主要seek操作
SDL_VIDEORESIZE视频大小变化事件,比如视频中间会出现大小变化,会触发该事件
SDL_QUIT、FF_QUIT_EVENT退出事件,如read_thread中出现各种异常会发送该消息
FF_ALLOC_EVENT事件比较特殊,如代码中的注释“ifthe queue is aborted, we have to pop the pending ALLOC event or wait for theallocation to complete”,该消息是video_thread中的发出的消息
总结
由于时间有限,文章有些虎头蛇尾,还请各位谅解。有多个方面没有详细分析,如音频处理和字幕处理部分,音视频同步,SDL显示等等很多很多有关的知识,这些知识对于我来说大部分也还是全新的东西,后续有机会还会继续学习和各位分享。
相关文章推荐
- FFPlay视频播放流程
- Ffplay视频播放流程
- FFmpeg视频播放流程
- Android视频播放数据读取的流程
- ffplay播放音频和视频命令
- [整理]Stagefright框架中视频播放流程
- 简化版ffplay中视频播放逻辑
- Chromium源码--视频播放流程分析(拨开云雾)
- 【FFmpeg】ffplay播放rtsp视频流花屏问题 (转)
- ffplay播放rtsp视频流花屏问题(含rtsp播放流程图)
- 【FFmpeg】ffplay播放rtsp视频流花屏问题
- vuforia6.2 在unity中图片识别播放视频流程
- 【FFmpeg】ffplay播放rtsp视频流花屏问题
- FFmpeg toturial04视频播放流程
- 【FFmpeg】ffplay播放rtsp视频流花屏问题
- 音视频的流程:录制、播放、编码解码、上传下载等
- 【FFmpeg】ffplay播放rtsp视频流花屏问题
- ffplay播放rtsp视频流花屏问题
- 【FFmpeg】ffplay播放rtsp视频流花屏问题
- FFmpeg音视频解码同步播放流程