Ffplay源码之read_thread解析(二)

2022-03-21 18:47:41 浏览数 (1)

前言:

大家好,我是小涂,本周继续给大家分享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内部继续执⾏耗时操作,直到完成既定的任务(⽐如读取到既定 的数据包)
代码语言:javascript复制

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打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参 数的初始化。

总结:

好了,这期内容就到这里了

0 人点赞