视频解码器
视频链接
FFmpeg 介绍
FFmpeg 库
FFmpeg 一共包含 8 个库:
- avcodec: 编解码(最重要的库)*
- avformat: 封装格式处理 *
- avfilter: 滤镜特效处理
- avdevice: 各种设备的输入输出
- avutil: 工具库(大部分库都需要这个库的支持)*
- postproc: 后加工
- swresample: 音频采样数据格式转换
- swscale: 视频像素数据格式转换 *
其中 * 表示本次课程中会涉及到的库
FFmpeg 解码流程
流程图
函数介绍
函数名 | 功能描述 |
---|---|
avformat_open_input |
打开输入文件并创建 AVFormatContext |
avformat_find_stream_info |
获取流信息 |
avcodec_find_decoder |
查找解码器 |
avcodec_alloc_context3 |
分配解码器上下文 |
avcodec_parameters_to_context |
复制编解码器参数 |
avcodec_open2 |
打开解码器 |
av_read_frame |
读取数据包 |
avcodec_send_packet |
发送数据包到解码器 |
avcodec_receive_frame |
接收解码后的帧 |
sws_scale |
像素格式转换 |
fwrite |
写入转换后的帧数据 |
av_packet_unref |
释放数据包 |
avcodec_free_context |
释放解码器上下文 |
avformat_close_input |
关闭输入流 |
FFmpeg 解码的数据结构
AV
表示 Audio Video
AVFormatContext
: 用于处理封装格式的上下文,包含视频最外层的信息
AVInputFormat
: 输入格式
AVStream
: 是一个数组,包含多个流,但是一般就包含视频流和音频流,第 0 个是视频流,第 1 个是音频流
AVCodecContext
: 编解码器上下文
AVCodec
: 编解码器,指明编码器的类型(h.264之类的)
AVPacket
: 压缩编码后的数据包
AVFrame
: 解码后的数据包
AVPacket
解码完为 AVFrame
AVFormatContext
用于处理封装格式的上下文,包含视频最外层的信息
- iformat: 输入视频的
AVInputFormat
- nb_streams: 输入视频的
AVStream
个数 - streams: 输入视频的
AVStream
数组 - duration: 输入视频的时长(以微秒为单位)
- bit_rate: 输入视频的码率
AVInputFormat
输入格式
- name: 输入视频的格式名称
- long_name: 输入视频格式的长名称
- extensions: 输入视频格式的扩展名
- id: 输入视频格式的 ID
- 一些封装格式处理的接口函数
AVStream
是一个数组,包含多个流,但是一般就包含视频流和音频流,第 0 个是视频流,第 1 个是音频流
- id: 输入视频流的 ID
- codecpar: 输入视频流的
AVCodecContext
- time_base: 输入视频流的时间基
- r_frame_rate: 输入视频流的帧率
time_base
是一个分数,表示时间基,用于将时间戳转换为实际时间。
r_frame_rate
是一个分数,表示帧率,用于计算帧间隔时间。
AVCodecContext
编解码器上下文
- codec:编解码器的
AVCodec
- width, height: 图像的宽高
- pix_fmt: 图像的像素格式
- sample_rate: 音频的采样率
- channels: 音频的声道数
- sample_fmt: 音频的采样格式
- codec_type: 编解码器的类型(视频、音频等)
- codec_id: 编解码器的 ID
AVCodec
编解码器,指明编码器的类型(h.264之类的)
- name: 编解码器的名称
- long_name: 编解码器的全称
- id: 编解码器的 ID
- 一些编解码的接口函数
AVPacket
压缩编码后的数据包,理解成装 h264
数据的盒子
- pts: 显示时间戳
- dts: 解码时间戳
- data: 压缩编码的数据
- size: 数据的大小
- stream_index: 所属的 AVStream (音频流还是视频流)
AVFrame
解码后的数据包,理解成装 yuv
数据的盒子
- data: 解码后的图像数据(音频采样数据)
- linesize: 对视频来说是图像中的一行像素的大小;对音频来说是整个音频帧的大小
- width, height: 视频帧的宽和高
- key_frame: 是否是关键帧
- pict_type: 帧类型(I, B, P 帧)
补充小知识
解码后的数据为什么要经过 sws_scale
转换?
解码后 YUV 数据格式的视频像素数据保存在 AVFrame 的 data[0],data[1],data[2],但是这些像素值并不是连续存储的,每行有效像素之后存储的是无效像素。
以亮度 Y 数据为例,data[0] 中一共包含了 linesize[0] * height 个数据。但是出于优化等方面考虑,linesize[0] 可能大于 width 。因此需要使用 sws_scale
进行转换。
转换后去除了无效数据,width 和 linesize[0] 就相等了。
代码运行
雷神给的这些代码,在资源释放上有些问题(资源的释放都集中在最后,如果程序提前终止,那就导致资源没有释放),但这里先不考虑这些。
代码
头文件 lib
和库文件 include
的配置在前面博客说到,可以复制到这个项目中(上一个博客我讲的是放在 usr/local/ffmpeg
)
这里代码和雷神视频中的代码略有不同,修改了很多:
|
|
CMakeLists.txt
|
|
手动编译一遍:
|
|
如果成功输出,则说明配置成功
没成功的话要注意看一下源码的路径和播放视频的路径是否正确
我这里源码放在 src/decoder.cpp
,播放视频放在video/input.mkv
调试
在左侧工具栏找到运行和调试
工具
点击创建 launch.json 文件
,选择C++(GDB/LLDB)
在launch.json
文件中添加以下内容:
|
|
按下 f5
即可调试
这里可能会遇到两个问题:
-
在源码位置按下
f5
后,会提示找不到找不到你链接的库
这里可能原因是没执行你的CMakeLists.txt
文件,换成点击左侧工具栏的启动按键就行 -
提示找不到输入文件,可以试试去掉
json
中cwd
字段的/build
:
|
|
后续博客就不讲调试的文件书写了,基本都差不多
练习
获取解码前的 h264 文件
注意这里只获取 MPEG-TS ,AVI 格式的文件,如果是别的文件,无法直接获取
这样重新编码后,就可以获取到解码前的 h264 文件了。
代码分析
- 打开文件
|
|
- 循环从媒体文件中读取一帧数据,并将其存储在
AVPacket
结构体中。
|
|
- 这个函数会读取下一个可用的数据包,无论是音频、视频还是其他类型的流
- 判断是否为视频帧
|
|
- 获取数据
|
|
- 关闭文件,释放资源
注意事项
-
输入文件一定要是
ts
或avi
格式!
因为mp4
和flv
格式需要解析moov
结构,而ts
和avi
格式可以直接解析h264
数据
我就是一开始没注意,导致浪费了很多时间 -
获取完数据记得释放资源和关闭文件
获取解码后的 yuv 文件
- 打开文件
|
|
- 循环从媒体文件中读取一帧数据,并将其存储在
AVPacket
结构体中。
|
|
- 判断当前帧是否为视频流
|
|
- 解码一帧视频数据
|
|
- 这个函数会将
AVPacket
中的压缩数据发送给解码器进行解码
- 接收解码后的数据
|
|
- 这个函数会从解码器中接收解码后的原始数据,并存储在
AVFrame
结构体中 AVERROR(EAGAIN)
是FFmpeg库中的一个错误码,表示当前没有足够的数据可供解码,需要等待更多数据到来才能继续解码。这种情况通常发生在数据流尚未准备好或缓冲区为空时AVERROR_EOF
表示已经到达数据流的末尾(End of File),没有更多的数据可供解码
- 处理解码后的数据
|
|
前面补充知识有提到,解码后的数据格式的视频像素值不是连续存储,而是按行存储的,会多出一些无效像素,导致像素的 width 和 linesize 不一致,要用 sws_scale
函数进行转换
sws_getContext
函数
- 用途:创建图像转换上下文 (
SwsContext
) - 参数说明:
pCodecCtx->width
,pCodecCtx->height
:原始视频帧的宽度和高度pCodecCtx->pix_fmt
:原始视频帧的像素格式AV_PIX_FMT_YUV420P
:目标像素格式,此处为 YUV420PSWS_BICUBIC
:缩放算法,使用双三次插值
sws_scale
函数
- 用途:执行实际的图像格式转换
- 参数说明:
img_convert_ctx
:之前创建的图像转换上下文(const unsigned char *const *)pFrame->data
:原始视频帧的数据pFrame->linesize
:原始视频帧的每行字节数0
:从原始帧的第 0 行开始转换pCodecCtx->height
:原始视频帧的高度pFrameYUV->data
:目标帧的数据缓冲区,用于存储转换后的 YUV420P 数据pFrameYUV->linesize
:目标帧的每行字节数
- 保存 YUV 数据到文件
|
|
- 关闭文件,释放资源
完整代码
代码优化了一些,也把一些要求输出的信息写在里面了
|
|
资源释放问题后面会再出博客写,因为涉及到各种细节