stream_component_open函数分析

news2024/11/20 7:21:15

stream_component_open() 函数主要作用是打开 音频流或者视频流 对应的解码器,开启解码线程去解码。

流程图如下:

stream_component_open() 的函数定义如下:

/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)

可以看到,函数的参数非常简单,第一个参数是 VideoState *is 全局管理器,第二个参数 stream_index 是 数据流 的索引值。


下面来分析 stream_component_open() 的函数里面的重点代码:

一开始的 avcodec_alloc_context3() 跟 avcodec_parameters_to_context() ,这可以说是常规操作了,就是申请一个解码器实例的内存,然后把容器流里面的信息拷贝过去。容器里面通常都是有编码器信息的。如果不理解这两个函数的作用,推荐阅读《如何使用FFmpeg的解码器》


第二个重点是,使用指定的编码器,例如你不用想 libx264 编码器,而是使用 openh264 编码器,就可以用 -c:v openh264 参数指定编码器。如下:

ffplay -c:v openh264 juren.mp4

也有另一种情况,就是容器里面记录的编码器信息是错误的,而你又知道正确的编码器信息,就可以强制指定。命令行的参数会赋值给 forced_codec_name 变量。命令的参数解析过程推荐阅读《FFplay命令行解析过程》


第三个重点,只有两个函数,filter_codec_opts() 跟 avcodec_open2() 。

filter_codec_opts() 这个函数实际上就是把命令行参数的相关参数提取出来。举个例子:

ffpaly -b:v 2000k -i juren-5s.mp4

上面的命令,指定了解码器的码率,但是他指定的是视频的码率,当 stream_component_open() 打开视频流的时候,这个 码率参数才会被 filter_codec_opts() 提取出来。

stream_component_open() 打开音频流的时候,b:v 不会被提取出来,因为这个参数是跟 视频流 相关的

所以你可以把 filter_codec_opts() 看成是一个处理命令行参数的函数,提取相关的参数。至于什么是相关,可以自行看这个函数的内部实现。

然后 avcodec_open2() 就会接受 filter_codec_opts() 返回 的 AVDictionary 参数。推荐阅读《如何设置编码器参数》


至此,解码器参数已经设置完毕,解码器也已经打开了了。


第四个重点是 把流属性设置为不丢弃, 就是下面这一句代码。

ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;

可以看到,stream_component_open() 函数会把打开的流的 discard 设置为 AVDISCARD_DEFAULT,这样这个流的数据就可以从 av_read_frame() 函数里面读出来了。

注意,ffplay.c 之前 在 read_thread() 函数里面,是把所有的流都设置为了 AVDISCARD_ALL,也就是会丢弃所有流的数据包。

所以,如果 mp4 里面有多个视频流,av_read_frame() 只会读取最好的那个视频流的包,音频流同理。

最后一个重点,就是一个 switch case 的逻辑,如下:

这段代码非常多,所以我把它缩进了。这里分别对 音频,视频,字幕做了区别处理。但是可以看到,音频的逻辑代码明显是最多的。

下面开始分析重点,如下:

首先可以看到,他有一个宏判断,大部分情况 AVFILTER 滤镜模块都是启用,所以不用管第二个 else。这里需要注意一下,虽然 ffplay -i juren-5s.mp4 这条命令没有使用滤镜,但是 ffplay 的逻辑还是会创建滤镜实例的,只不过这是一个空的实例。这样做是为了代码逻辑更加通用。

无论命令行参数使不使用滤镜,他都是同样的逻辑。

然后需要注意上图中的 is->audio_filter_src 变量,这个变量存储的实际上是从解码器出来的音频信息。然后调 configure_audio_filters() 这个函数来创建音频流的滤镜

configure_audio_filters() 函数最重要的地方就是搞好了 is->in_audio_filter 跟 is->out_audio_filter 两个滤镜。解码器输出 AVFrame 之后需要往 in_audio_filter 里面丢,然后播放的时候,需要从 out_audio_filter 读取 AVFrame

configure_audio_filters() 函数的分析请看《FFplay音频滤镜分析》一文。

后面的av_buffersink_get_sample_rate() 等函数的调用实际上就是从 buffsink 出口滤镜里面获取到最后的音频信息。


第二个重点如下:

上图中,红色圈是重点,绿色圈不是重点,绿色圈出来的变量 是用在 音频向视频同步的场景上的,非常难懂,而且应用场景极少。通常音视频同步都是以音频时钟为准。所以这段代码可以暂时不管,音频向视频同步这东西你学了基本也用不上,不过可以用来装下逼。

audio_open() 函数的内部逻辑就是调 SDL_OpenAudioDevice() 打开音频设备,不过由于音频设备各种各样,从 buffersink 滤镜出来的音频帧,不一定被硬件设备支持,所以可能需要降低采样率之类。例如:有些比较差的音响不支持太高采样或者太多的声道数。

audio_open() 函数会选出被硬件设备支持的采样率,声道数 去打开。这些最终的声道数,采样率等信息,就放在 is->audio_tgt 变量返回。

所以 audio_open() 函数的重点是,打开音频设备,并且把最终的音频信息放在 is->audio_tgt 变量里面了。

更详细的分析请阅读《audio_open函数分析》


接着,会把 audio_tgt 拷贝给 audio_src,如下:

is->audio_src = is->audio_tgt;

这句代码看起来会有点莫名其妙,为什么把 audio_tgt 赋值给 audio_src 呢?

首先 is->audio_src 是一个 struct AudioParams ,一个存储音频格式信息的结构体。变量名里有个 src ,代表音频的源头,也就是音频源头的格式是怎样的。但是注意这个源头不是指 MP4 文件里面的音频格式,虽然这个也是源头。

但是它的 src 指的是 is->swr_ctx 重采样实例的源头,也就是当需要进行重采样的时候,要输入给 is->swr_ctx 的原始音频格式就是 is->audio_src。流程图如下:

上面的流程图看起来比较容易理解,这是需要重采样的流程,但是不一定总是需要重采样的,当 buffersink 出口滤镜出来的音频格式,跟打开硬件设备时候的音频格式(is->audio_tgf)一致的时候,就不需要重采样了。

上面的流程图,如果去掉重采样,是不是就直接是 is->audio_src = is->audio_tgt; 了?

因此 is->audio_src 存储的其实是 buffersink 出口滤镜的音频格式,但是因为出口滤镜的音频格式可能跟 is->audio_tgt 本身是一样的,所以它上面那句代码就这样写了。

buffersink 跟 audio_tgt 音频格式不一样,就需要重采样。从重采样实例 is->swr_ctx 角度来看, is->audio_src 确实是源头。只是他的代码取巧了一下。

先剧透一下后面 audio_decode_frame() 函数中的重采样代码,如下

小总结:ffplay 有两个处理音频的地方,一个是 滤镜(is->agraph),一个是重采样(is->swr_ctx)。


最后,就是记录播放的音频流信息,其他的视频流,字幕流也有类似的操作,如下:

is->audio_stream = stream_index;
is->audio_st = ic->streams[stream_index];

最后一个重点就是调用 decoder_init() 与 decoder_start(),如下:

decoder_init() 函数是比较简单的,不过它用了一个新的数据结构 struct Decoder,所以我们先讲一下这个结构,如下:

typedef struct Decoder {
    AVPacket *pkt; //要进行解码的 AVPacket,也是要发送给解码器的 AVPacket
    PacketQueue *queue; // AVPacket 队列
    AVCodecContext *avctx; //解码器实例
    int pkt_serial; //序列号
    int finished; //已完成的时候,finished 等于上面的 pkt_serial。当 buffersink 输出 EOF 的时候就是已完成。
    int packet_pending; //代表上一个 AVPacket 已经从队列取出来了,但是未发送成功给解码器。未发生成功的会保留在第一个字段 pkt 里面,下次会直接发送,不从队列取。
    SDL_cond *empty_queue_cond; //条件变量,AVPacket 队列已经没有数据的时候会激活这个条件变量。
    int64_t start_pts; //流的第一帧的pts
    AVRational start_pts_tb; //流的第一帧的pts的时间基
    int64_t next_pts; //下一帧的pts,只有音频用到这个 next_pts 字段
    AVRational next_pts_tb; //下一帧的pts的时间基
    SDL_Thread *decoder_tid; //解码线程 ID。
} Decoder;

我讲解讲一下 struct Decoder 结构的一些字段,首先是第一个 AVPacket *pkt ,这个实际上就是从 AVPacket 队列拿出来的。然后把这个 pkt 发送给解码器,如果发送成功,那当然是 unref 这个 pkt,但是如果发送给解码器失败,就会把 packet_pending 置为1,pkt 不进行 unref,下次再继续发送。

pkt_serial 这个序列号,推荐阅读《FFplay序列号分析》。

还有一个需要讲解的是 next_pts 字段,一些读者可能会疑惑,不是每一个 AVFrame 都有 pts 的吗? 为什么还需要这个 next_pts 这个字段?

这就是因为解码出来的 AVFrame 的 pts 有些是 AV_NOPTS_VALUE,这时候就需要 next_pts 来纠正。

next_pts 的计算规则就是上一帧的 pts 加上他的样本数(也就是播放多久)。

注意:视频流没有使用 next_pts 来纠正,只有音频流用了 next_pts,如下:


接下来分析decoder_init() 函数,代码如下:

static int decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->pkt = av_packet_alloc();
    if (!d->pkt)
        return AVERROR(ENOMEM);
    d->avctx = avctx;
    d->queue = queue;
    d->empty_queue_cond = empty_queue_cond;
    d->start_pts = AV_NOPTS_VALUE;
    d->pkt_serial = -1;
    return 0;
}

可以看到,就是做一些赋值,比较简单,但是也有一个重点,就是他的 empty_queue_cond 实际上就是 continue_read_thread,只是换了个名字。


接着分析下一个函数 decoder_start(),代码如下:

static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
{
    packet_queue_start(d->queue);
    d->decoder_tid = SDL_CreateThread(fn, thread_name, arg);
    if (!d->decoder_tid) {
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    return 0;
}

比较简单,就是开启 SDL 解码线程


至此,switch case 里面对于音频的处理就讲解完毕,对于视频的处理更加简单,仅仅调了 decoder_init() 与 decoder_start(),如下:

stream_component_open()函数分析完毕,


 推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106532.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8S知识点及dashboard操作

1.什么是K8S? K8S是一组服务器集群,可以在集群的各个节点上运行特定的容器。 K8S所管理的是:集群节点上的容器 特性: 自我修复,弹性伸缩(根据实时服务器的并打情况,增加或收缩容器数量&…

网络编程套接字Socket(通过两个用例,逐行注释,详细理解)干活满满建议收藏

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言1.分类1.流套接字2.数据报套接字3.原始套接字2.Socket通信模型 3.UDP套接字编程1. DatagramSocket API1.构造方法1.DatagramSocket()2.DatagramSocket(int port)…

C语言之复合类型上卷(十八)(阴阳两极)

上一篇: C语言之内存管理(十七)(转世灵童现世) 逐梦编程,让中华屹立世界之巅。 简单的事情重复做,重复的事情用心做,用心的事情坚持做; 文章目录前言一、什么是结构体?二、结构体的定义及初始化…

USB TO SPI(上海同旺电子)调试器调试MCP3201 A/D 转换器

所需设备: 1、USB TO SPI(上海同旺电子); 2、MCP3201 12 位A/D 转换器; 特性 • 12 位分辨率 • 1 LSB DNL (最大值) • 1 LSB INL (最大值)(MCP3201-B) • 2 LSB INL &#xff…

pdf文件太大怎么变小,如何压缩pdf大小

pdf文件太大怎么变小?如果你是Windows电脑,可以使用PDF编辑器来减小PDF文件的大小,比如这款出色的PDF压缩工具-易我PDF编辑器,它的“压缩”功能提供了两种减小文件大小的方法,这使得它既适合那些只想获得更小的PDF的人…

【vscode】c++程序的自动编译及调试(环境centos)

目录1.新增配置文件(1)c_cpp_properties.json(2)files.associations(3)tasks.json(4)CMakeLists.txt2.断点调试1.新增配置文件 VS Code的配置文件一般是指特定目录下的JSON文件。所谓JSON是一种文本格式&a…

LCF-ATEPC(2020 Elsevier)面向中文的方面级提取和分类

论文题目(Title):A Multi-task Learning Model for Chinese-oriented Aspect Polarity Classification and Aspect Term Extraction (面向中文的方面极性分类和方面项提取的多任务学习模型) 研究问题(Question&#…

适用于 Windows 10/11 电脑 的 5 大好用的离线录屏软件

屏幕录制应用程序可以数字记录出现在任何设备或 PC 屏幕上的内容,并同时以高清流式传输音频和视频。 因此,他们帮助创建营销视频、跟踪客户行为、设计产品演示、监控员工活动、录制教育内容、网络研讨会内容和业务会议内容。 现在您已经意识到屏幕录…

VS系列多通道振弦传感器无线采发仪的数据发送说明

每次设备启动后会将采集到的传感器数据进行内部存储,并在设置好的时间间隔将数据发送出去,通过修改“数据发送方式”参数,监测数据可由数据接口输出也可经由无线网络发送。在发送监测数据时,可通过修改“数据包协议”参数来设置所…

函数和数组习题

个人主页:平行线也会相交 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创 收录于专栏【C语言基础习题】 文章目录知识点习题2.实现一个整型数组的冒泡排序(编程体)。3.编程题:创建一个整型…

springcloud,springboot各个版本之间的关系

1 版本关系 在实际的开发中如果要自己搭建矿建,发现springcloud,springboot的版本可能是首先需要确定的,那么他们之间的关系是什么呢?看官网,地址 Spring Cloud 左侧是cloud的版本,右侧是对应的文档&…

Splunk Window 客户端迁移

最近客户的Splunk deployment server 要迁移,伴随着client 端的配置也要相应的调整: 先看一下架构: 看一下主要的参数: Summary of key terminology Heres a recap of the key definitions: TermMeaningdeployment serverA Splunk Enterprise instance that acts as a c…

Java中的日期与时间

Java中的日期与时间\huge{Java中的日期与时间}Java中的日期与时间 JavaJavaJava中有很多类是专门用于描述日期类的。 Date类 DateDateDate类:用于表示当前所在系统的日期时间信息。 Date类的构造器 示例: Date d new Date(); System.out.println(d);…

12月第3周榜单丨B站UP主排行榜(飞瓜数据B站)发布!

飞瓜轻数发布2022年12月12日-12月18日飞瓜数据UP主排行榜(B站平台),通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况,为用户提供B站号综合价值的数据参考,根据UP主成长情况用户能够快速找到运营能力强的…

Redis高级篇

redis的四个问题: 1.Redis是基于内存存储,服务重启可能会丢失数据; 2.并发能力问题:单节点Redis能力虽然不错,但也无法满足如618这种高并发的场景(618并发 数量达到数十万甚至上百万); 3.如果reids宕机,服务不可用,则需要一种自动的故障恢复手段; 4.存…

自学Python可以找到工作吗?

自学Python可以找到工作吗?自学Python找工作主要看自己的学习能力,自学能力很强学完并精通当然可以工作,不过对于大多数人而言一般都挺难,学习不成系统,遇到问题没人解决很容易放弃半途而废。 学Python能干很多很多事…

NodeJS安装-Vue模块化项目构建

NodeJS安装-Vue模块化项目构建 一、环境准备(NodeJS安装) 1. 安装NodeJS 官网自行下载,并安装 2. 配置npm的全局安装路径 npm config set prefix "D:\soft_install\dev\qianduan_dir\nodejs"3. 切换npm的淘宝镜像&#xff0c…

C++11标准模板(STL)- 算法(std::is_permutation)

定义于头文件 <algorithm> 算法库提供大量用途的函数&#xff08;例如查找、排序、计数、操作&#xff09;&#xff0c;它们在元素范围上操作。注意范围定义为 [first, last) &#xff0c;其中 last 指代要查询或修改的最后元素的后一个元素。 判断一个序列是否为另一个…

《记忆力心理学》5个方法 让你过目不忘

《记忆力心理学》 关于作者 赫尔曼•艾宾浩斯&#xff0c;西方心理学泰斗级人物&#xff0c;生活在19世纪的德国心理学家。他是最早用实验的方法对记忆进行量化研究的 人&#xff0c;最受人瞩目的贡献&#xff0c;就是发现了记忆保持曲线。 关于本书 这本书可以看作是记忆心…

全国职业院校技能大赛网络搭建与应用赛项——云平台底层的一些命令

1.列出运行的虚拟机 virsh list 2.列出所有的虚拟机virsh list --all 3.网络信息表&#xff08;20分&#xff09; 显示网络名及所在的vlan idopenstack network show Network10 -c name -c provider:segmentation_id openstack network show Network20 -c name -c provider:s…