ffmpeg 完美实现解封装操作!
一、前言
大家好,很长一段时间没有继续更新 ffmpeg 的相关技术文章了,最近更多的时间和精力主要集中在给自己不断灌入新的知识,所以接下来只要有时间就会疯狂输出所学习到的技术干货!
今天我们要分享的主要音视频里面的解封装过程详细解析;在讲解解封装之前,我们简单的来了解一下流媒体文件是如何被播放出来的,要实现播放,那这个过程到底要经历哪些技术处理呢?一般一个音视频流媒体文件播放实现流程图如下:
流媒体文件如何实现播放流程
从上面的流程图中,我们可以发现一个流媒体文件播放实现过程,看上去是不怎么复杂,但是其实里面有很多细小的技术点;今天暂时我们先来掌握解封装!
二、探索解封装的奥秘
1、什么是解封装呢?
在了解什么是解封装之前,不知道大家平时在自己的电脑里面播放视频文件的时候,有没有注意视频文件的后缀格式呢,比如下面几种文件格式:
常用的几种封装格式
上面的 mp4、flv、ts 等都是对音视频数据进行封装的一种封装格式,通俗的讲,就把很多东西合成一个东西,只是合成的这个东西,表现形式不一样而已,用更加的专业术语来讲,这里的合成就是复用器,我们可以用一张图来解释:
复用器
那么听了上面的解说,你自然而然的就会想到解复用器了,那么也就是解封装了,解封装的作用就跟上面的复用器起着相反的作用,就是把一个流媒体文件,拆解成音频数据和视频数据(专业的讲,一般被拆解成 H.264 编码的视频码流和 AAC 编码的音频码流),下面还是用一张图来解释:
解封装(解复用器)
三、利用 ffmpeg 接口实战解封装实现
经过上面的讲解,想必大家对解封装的概念已经非常清楚了;那么接下来呢,我们就可以利用 ffmpeg 里面的 libavformat 库(它是一个包含用于多媒体容器格式的解复用器和复用器的库,里面有很多可供我们开发人员进行实战操作的 api。)调用相关 api 来实现解封装的具体操作。
1、工欲善必先利其器:
在开始写代码实现之前呢,我们还要了解一下解封装的一个具体流程和相应的 api。我们先把解封装实现相应的 api 接口得介绍一下,不然很多朋友直接看代码实现不知道什么意思,而且也不知道这些接口说明去哪里找(这个曾经在交流的时候,还真有人不知道 api 接口里面传的参数是什么意思,其实吧,ffmpeg 官网手册 api 接口介绍里面有非常详细的介绍呢,或者 ffmpeg 源码里面也有 api 接口的详细说明使用!);当然如果有时间,我觉得非常有必要去研究一下 ffmpeg 的源码阅读,千万不要停留在只会调用 api 的层次,更多的是我们要了解背后深层次的东西;源码阅读,我目前在阅读 4.2.1 版本的 ffmpeg 源码:
ffmpeg 4.2.1 版本源码
好了,下面我们开始介绍解封装相关的接口和结构体说明;第一时间,大家可以去官网找到 ffmpeg 的 api 接口说明文档:
解封装常用的 api 如下:
avformat_alloc_context():负责申请一个 AVFormatContext 结构体的内存,并进行初始化,它的函数原型如下:
avformat_free_context():释放 AVFormatContext 结构体里面的所有东西以及该结构体本身,函数原型如下:
avformat_open_input():从函数名称就知道是打开要输入的流媒体文件,函数原型如下:
参数说明:
ps:指向用户提供的 AVFormatContext 的指针(由 avformat_alloc_context 分配)。可能是指向 NULL 的指针,在这种情况下,此函数将分配 AVFormatContext 并将其写入 ps。请注意,用户提供的 AVFormatContext 将在失败时释放。
url:要打开的流的 url,也就是要打开的流媒体文件。
fmt:如果为非 NULL,则此参数强制使用特定的输入格式。否则,将自动检测格式。
options:包含 AVFormatContext 和 demuxer-private 选项的字典。返回时,此参数将被销毁并替换为包含未找到的选项的 dict。可能为 NULL。
注意:返回值为 0 的时候表示成功,失败的时候返回 AVERROR,跟 linux 里面的 api 接口机制类似。
avformat_close_input():关闭打开的输入 AVFormatContext,释放它及其所有内容,并将*s 设置为 NULL;关闭后就不需要再调用 avformat_free_context()进行释放了。它的函数原型如下:
avformat_find_stream_info():读取媒体文件的数据包以获取流信息,这对于没有标题的文件格式(例如 MPEG)很有用。在 MPEG-2 重复帧模式的情况下,此功能还可以计算实际帧率。该功能不会更改逻辑文件的位置。被检查的分组可以被缓冲以用于以后的处理。函数原型如下:
参数说明:
ic:传入的流媒体文件
options:如果为非 NULL,则是指向字典的 ic.nb_streams 长指针数组,其中第 i 个成员包含与第 i 个流相对应的编解码器选项。返回时,每本词典将填充未找到的选项。
注意:此函数不能保证打开所有编解码器,因此选项在返回时为非空是完全正常的行为。
av_read_frame():返回流的下一帧;此函数返回文件中存储的内容,并且不验证解码器是否存在有效的帧。它将文件中存储的内容拆分为多个帧,并为每个调用返回一个帧。它不会忽略有效帧之间的无效数据,从而为解码器提供可能的最大解码信息;如果 pkt-> buf 为 NULL,则该数据包在下一个 av_read_frame()或 avformat_close_input()之前一直有效。否则,数据包将无限期有效。在这两种情况下,当不再需要该数据包时,都必须使用 av_packet_unref 释放它。对于视频,数据包恰好包含一帧。对于音频,如果每个帧具有已知的固定大小(例如 PCM 或 ADPCM 数据),则它包含整数个帧。如果音频帧具有可变大小(例如 MPEG 音频),则它包含一帧。始终将 pkt-> pts,pkt-> dts 和 pkt-> duration 设置为以 AVStream.time_base 为单位的正确值(并猜测格式是否无法提供它们)。如果视频格式具有 B 帧,则 pkt-> pts 可以为 AV_NOPTS_VALUE,因此,如果不对有效载荷进行解压缩,则最好依靠 pkt-> dts。
函数原型如下:
注意:返回值为 0 时,表示成功,非 0 表示失败!
avformat_seek_file():寻求时间戳记(或者说定位文件位置);将进行搜索,以便可以成功呈现所有活动流的点将最接近 ts,并且在 min / max_ts 之内。活动流是所有具有 AVStream.discard <AVDISCARD_ALL 的流。如果标志包含 AVSEEK_FLAG_BYTE,则所有时间戳均以字节为单位,并且为文件位置(并非所有解复用器均支持)。如果标志包含 AVSEEK_FLAG_FRAME,则所有时间戳都在具有 stream_index 的流中的帧中(并非所有解复用器均支持)。否则,所有时间戳均以 stream_index 选择的流为单位,或者如果 stream_index 为-1,则以 AV_TIME_BASE 单位。如果标志包含 AVSEEK_FLAG_ANY,则将非关键帧视为关键帧(并非所有解复用器均支持此关键帧)。如果标志包含 AVSEEK_FLAG_BACKWARD,则将其忽略。
函数原型如下:
参数说明:
s:媒体文件句柄
stream_index:流的索引,用作时基参考
min_ts:最小可接受时间戳
ts:目标时间戳
max_ts:最大可接受时间戳
flag:标志
注意:>=0 表示返回成功,否则都是失败;同时要注意这是仍在构建中的新 seek API 的一部分。因此,请不要使用此功能。它可能随时更改,不要期望与 ABI 兼容
2、解封装相关结构体介绍:
AVFormatContext:从上面的 api 介绍中,我们可以经常看到这个结构体,它的重要性不言而喻了,它存储了音视频封装格式含有的信息,这里我不做具体介绍,列了几个出来,感兴趣的朋友可以去 Avformat.h 中查看:
大致简化为:
AVStream:表示存储每一个音频和视频流的信息。它也是在头文件 AVformat.h 里面查看:
大致简化为:
3、代码实现框架:解封装流程
上面已经介绍了 api 和解封装结构体,剩下的就是我们该如何实现解封装的核心思想了,有了核心思想,我们就可以达到要实现的解封装效果了:具体流程图如下:
解封装实现流程图
四、解封装具体实现代码:
我这里开发环境是在 qt 下进行开发的,播放的是本地文件:
最终运行效果如下:
运行结果
注意:不同封装格式的流媒体文件被解封装打印出来的信息是不同的,这点要注意!
五、总结:
今天的分享就到这里了,如果你也喜欢音视频开发,可以相互交流,一起进步;同时如果大家看到我没更新,不是在偷懒,一般是在给自己疯狂输入;好了,我们下期见!
评论