前言:
大家好,我是小涂,本周继续给大家分享ffplay中的read_thread这个线程源码的解读,这算是自己的一个学习记录过程吧。
在分享之前我还是会贴出那张框架图出来:
咋们现在还是停留在把本地或者网络上的流媒体文件通过解复用,然后把这个流媒体文件里面的音频、视频、字幕数据给分离出来,然后再把这些数据送进相关队列里面去,后面就会涉及到解码操作了,这个解码的源码解析,先等把这个read_thread分享再进行分享。好了,那么就正式开始本期的内容分享吧:
一、read_thread线程源码解读:
在上周我们已经找到了read_thread这个线程的入口了:
为了方便阅读,这次分享,我用图片的方式贴出源码,然后再一段一段小源码解读,这样就不会看着一大坨代码看着不方便:
这部分代码没啥好说的,都是一些初始化操作。
这里的interrupt_callback需要讲一下:
代码语言:javascript复制ic->interrupt_callback.callback =
decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
这里设置中断回调函数,如果出错或者退出,就根据⽬前程序设置的状态选择继续check或者直接退出。那么什么情况要进行退出呢?当执⾏耗时操作时(⼀般是在执⾏while或者for循环的数据读取时),会调⽤interrupt_ callback.callback,那么就会调用这个回调函数decode_interrupt_cb::
- 回调函数中返回1则代表ffmpeg结束耗时操作退出当前函数的调⽤
- 回调函数中返回0则代表ffmpeg内部继续执⾏耗时操作,直到完成既定的任务(⽐如读取到既定 的数据包)
static int decode_interrupt_cb(void *ctx)
{
VideoState *is = ctx;
return is->abort_request;
}
如果看到这里听我这么解释可能还是会比较懵,所以为了弄懂我们在进行播放读取数据的时候,如何触发interrupt_ callback的整个路线,为此这里我用gdb来调试追踪,不过这里稍微要注意一些,我在之前写过一篇文章关于源码安装ffmpeg,并进行了编译,现在就派上用场了:
我们可以通过 gdb ./ffplay_g来播 放视频,然后在decode_interrupt_cb打断点。下面是我之前编译出来的结果:
下面用gdb来启动它来:
进行设置断点:
这个时候还要进行运行,也就是要播放一个视频文件,不然等下使用bt命令是看不到整个它调用路径的:
现在我们就可以用栈帧来查看触发的路径了:
代码语言:javascript复制#0 decode_interrupt_cb (ctx=0x7ffff7e36040) at fftools/ffplay.c:2715
#1 0x00000000007d99b7 in ff_check_interrupt (cb=0x7fffd40012f0) at libavformat/avio.c:667
#2 retry_transfer_wrapper (transfer_func=0x7dd950 <file_read>, size_min=1, size=32768, buf=0x7fffd4001540 "", h=0x7fffd40012c0) at libavformat/avio.c:374
#3 ffurl_read (h=0x7fffd40012c0, buf=0x7fffd4001540 "", size=32768) at libavformat/avio.c:411
#4 0x000000000068cd9c in read_packet_wrapper (size=<optimized out>, buf=<optimized out>, s=0x7fffd40095c0) at libavformat/aviobuf.c:535
#5 fill_buffer (s=0x7fffd40095c0) at libavformat/aviobuf.c:584
#6 avio_read (s=s@entry=0x7fffd40095c0, buf=0x7fffd40096d0 "@225", size=size@entry=2048) at libavformat/aviobuf.c:677
#7 0x00000000006b7780 in av_probe_input_buffer2 (pb=0x7fffd40095c0, fmt=0x7fffd4000948, filename=filename@entry=0x36429b0 "../../share/2_audio.mp4",
logctx=logctx@entry=0x7fffd4000940, offset=offset@entry=0, max_probe_size=1048576) at libavformat/format.c:262
#8 0x00000000007b631d in init_input (options=0x7fffe19b6b50, filename=0x36429b0 "../../share/2_audio.mp4", s=0x7fffd4000940) at libavformat/utils.c:443
#9 avformat_open_input (ps=ps@entry=0x7fffe19b6bf8, filename=0x36429b0 "../../share/2_audio.mp4", fmt=<optimized out>, options=0x2e38450 <format_opts>)
at libavformat/utils.c:573
通过上面的追踪,我们可以发现在这个地方有触发到:
好了,这个手段一般都是我们对陌生代码调试追踪的一个小技巧。
下面我继续往下看代码:
代码语言:javascript复制//特定选项处理
if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
scan_all_pmts_set = 1;
}
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
if (err < 0) {
print_error(is->filename, err);
ret = -1;
goto fail;
}
if (scan_all_pmts_set)
av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);
if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
av_log(NULL, AV_LOG_ERROR, "Option %s not found.n", t->key);
ret = AVERROR_OPTION_NOT_FOUND;
goto fail;
}
scan_all_pmts是mpegts的⼀个选项,表示扫描全部的ts流的"Program Map Table"表。这⾥在没有设定 该选项的时候,强制设为1。
然后我们可以看到接口avformat_open_input,它的作用主要为:
源码注解:
代码语言:javascript复制/**
* Open an input stream and read the header. The codecs are not opened.
* The stream must be closed with avformat_close_input().
*
* @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context).
* May be a pointer to NULL, in which case an AVFormatContext is allocated by this
* function and written into ps.
* Note that a user-supplied AVFormatContext will be freed on failure.
* @param url URL of the stream to open.
* @param fmt If non-NULL, this parameter forces a specific input format.
* Otherwise the format is autodetected.
* @param options A dictionary filled with AVFormatContext and demuxer-private options.
* On return this parameter will be destroyed and replaced with a dict containing
* options that were not found. May be NULL.
*
* @return 0 on success, a negative AVERROR on failure.
*
* @note If you want to use custom IO, preallocate the format context and set its pb field.
*/
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
- ⽤于打开输⼊⽂件(对于RTMP/RTSP/HTTP⽹络流也是⼀样,在ffmpeg内部都 抽象为URLProtocol,这⾥描述为⽂件是为了⽅便与后续提到的AVStream的流作区分),读取视频⽂件 的基本信息。参数是fmt和options。通过fmt可以强制指定视频⽂件的封装,options可以传递额外参数 给封装(AVInputFormat)。
接着我们继续往下读:
这里我们可以看到接口avformat_find_stream_info()。在打开了⽂件后,就可以从AVFormatContext中读取流信息了。⼀般调⽤avformat_find_stream_info获 取完整的流信息。为什么在调⽤了avformat_open_input后,仍然需要调⽤avformat_find_stream_info 才能获取正确的流信息呢:
- 该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说 MPEG(⾥应该说ts更准确)(FLV⽂件也是需要读取packet 分析流信息)。⽽被读取⽤以分析流信息的数 据可能被缓存,供av_read_frame时使⽤,在播放时并不会跳过这部分packet的读取。
接着往下看:
代码语言:javascript复制 /* 检测是否指定播放起始时间 */
if (start_time != AV_NOPTS_VALUE) {
int64_t timestamp;
timestamp = start_time;
/* add the stream start time */
if (ic->start_time != AV_NOPTS_VALUE)
timestamp = ic->start_time;
// seek的指定的位置开始播放
ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
if (ret < 0) {
av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3fn",
is->filename, (double)timestamp / AV_TIME_BASE);
}
}
如果指定时间则seek到指定位置avformat_seek_file。可以通过 ffplay -ss 设置起始时间,时间格式hh:mm:ss,⽐如 ffplay -ss 00:00:30 test.flv 则是从30秒的起始位置开始播放。
接着往下读:
这里可以根据用户来查找流,比如说,我们在播放的时候,可以指定音频流或者视频流、字幕流,可以用ffplay播放命令来指定:
下面是我在qt里面进行演示的效果:
我们接着往下读:
这里如果用户没有指定特定流的话,则ffplay主要是通过 av_find_best_stream 来选择,所以有下面几种情况:
- 如果⽤户没有指定流,或指定部分流,或指定流不存在,则主要由av_find_best_stream发挥作⽤。
- 如果指定了正确的wanted_stream_nb,⼀般情况都是直接返回该指定流,即⽤户选择的流。
- 如果指定了相关流,且未指定⽬标流的情况,会在相关流的同⼀个节⽬中查找所需类型的流,但⼀般结 果,都是返回该类型第1个流。
我们接着往下读:
代码语言:javascript复制//从待处理流中获取相关参数,设置显示窗⼝的宽度、⾼度及宽⾼⽐
is->show_mode = show_mode;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
AVCodecParameters *codecpar = st->codecpar;
//根据流和帧宽⾼⽐猜测帧的样本宽⾼⽐。
//由于帧宽⾼⽐由解码器设置,但流宽⾼⽐由解复⽤器设置,因此这两者可能不相 等。
//此函数会尝试返回待显示帧应当使⽤的宽⾼⽐值。
//基本逻辑是优先使⽤流宽⾼⽐(前提是值是合理的),其次使⽤帧宽⾼⽐。
//这样,流宽⾼⽐(容器设置,易于修改)可以覆盖帧宽⾼⽐
AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
if (codecpar->width)
// 设置显示窗⼝的⼤⼩和宽⾼⽐
set_default_window_size(codecpar->width, codecpar->height, sar);
}
这⾥实质只是设置了default_width、default_height变量的⼤⼩,没有真正改变窗 ⼝的⼤⼩。真正调整窗⼝⼤⼩是在视频显示调⽤video_open()函数进⾏设置。
接着往下读:
现在通过前面的操作,⽂件打开成功,且获取了流的基本信息,并选择⾳频流、视频流、字幕流。接下来就可以所选流对应的解码器了。
代码语言:javascript复制 /* 打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。 */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {// 如果有音频流则打开音频流
stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { // 如果有视频流则打开视频流
ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
}
if (is->show_mode == SHOW_MODE_NONE) {
//选择怎么显示,如果视频打开成功,就显示视频画面,否则,显示音频对应的频谱图
is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;
}
if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) { // 如果有字幕流则打开字幕流
stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
}
if (is->video_stream < 0 && is->audio_stream < 0) {
av_log(NULL, AV_LOG_FATAL, "Failed to open file '%s' or configure filtergraphn",
is->filename);
ret = -1;
goto fail;
}
由于文章篇幅原因,我们下周继续,read_thread线程里面还有一部分没有分析玩,同时这个 stream_component_open()接口里面内容比较多,我们下期再细细分析。
现在总结一下上面经历了哪些操作流程:
avformat_alloc_context 创建上下⽂
avformat_open_input打开媒体⽂件
avformat_find_stream_info 读取媒体⽂件的包获取更多的stream信息
检测是否指定播放起始时间,如果指定时间则seek到指定位置avformat_seek_file
查找查找AVStream,讲对应的index值记录到st_index[AVMEDIA_TYPE_NB]:
代码语言:javascript复制a. 根据⽤户指定来查找流avformat_match_stream_specifier
b. 使⽤av_find_best_stream查找流
通过AVCodecParameters和av_guess_sample_aspect_ratio计算出显示窗⼝的宽、⾼
stream_component_open打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参 数的初始化。
总结:
好了,这期内容就到这里了