前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Ffplay源码之read_thread解析(二)

Ffplay源码之read_thread解析(二)

作者头像
用户6280468
发布2022-03-21 18:47:41
6070
发布2022-03-21 18:47:41
举报
文章被收录于专栏:txp玩Linux

前言:

大家好,我是小涂,本周继续给大家分享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.3f\n",
                   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 filtergraph\n",
               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打开⾳频、视频、字幕解码器,并创建相应的解码线程以及进⾏对应输出参 数的初始化。

总结:

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 txp玩Linux 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一、read_thread线程源码解读:
  • 总结:
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档