0 引子
在FFmpeg的众多库函数中,libavformat和libavcodec分别实现了封装相关和编解码相关功能,通过这些库函数能够实现视频裸数据(.yuv)到封装后的文件(.mp4)的互转。就稍微记录一下编解码相关的流程和相关的代码实现。
首先我们需要一个yuv文件和一个mp4文件,可以通过以下命令实现(需要安装ffmpeg):
|
|
顺便一提,裸数据和压缩封装后的视频文件的大小差别真的好大:
|
|
0.1基础概念
这是我理解的一个视频封装格式的内容:若干个视频流、若干个音频流、若干个字母流以及元数据,同时还有文件头和文件尾;在这次实验中只有一个视频流。
frame是一帧在解码后(也就是原始数据)的内容,packet通常表示在一个frame在编码后的数据,随后就将数据写入到媒体容器中。
1 yuv ==> mp4
1.1 大致流程
由于裸视频文件不包含文件头,因此我们需要手动设置视频的分辨率和帧率。
总的来说大致就是以下步骤:
-
读取yuv文件;
-
读取yuv文件的分辨率(宽和高)和帧率,这三项以及yuv文件的像素格式是编码过程中必须的;
-
设置封装后的文件路径;
-
选择一种编码方式(264/265, etc),根据这种编码方式创建一个编码器以及编码上下文;
-
将视频的基本参数(分辨率、帧率、像素格式)设置完成后将编码上下文进行初始化;
-
创建并初始化封装;
-
设置媒体流的编码信息,这个编码信息至少包含四项(这个是在我的测试之后给出来的,在其他编码器中可能需要更多参数):
- 流类型;
- 编码器类型
- 视频宽
- 视频高
-
打开输出文件;
-
在视频文件中写入数据头;
-
创建帧数据,其中包含:
- 像素格式
- 视频宽
- 视频高
设置参数之后要使用av_frame_get_buffer()分配对应的内存空间;
-
初始化packet,声明packet后要使用av_new_packet()分配内存空间,不然就会出现总线错误(一把辛酸泪)
-
开始循环读取yuv文件中的yuv分量,读取一遍之后对帧计数器进行自增,如果读到数据就执行如下步骤:
- 将帧发送给编码器进行编码;
- 从编码器获取编码后的数据,保存在packet中(也就是一个packet保存了一个视频帧的数据),当读取到数据之后执行以下操作:
- 转换时间基
- 将packet中的数据放到封装器中进行封装
- 清空packet中的数据;
- 然后回到步骤12
-
写视频尾;
-
释放相关的编码和封装结构
-
关闭裸视频文件;
1.2 代码
|
|
2 mp4 => yuv
2.1 大致流程
在前面说过,一个媒体封装格式中可能会有多个流,因此在读取mp4文件的时候要先识别出来视频流是哪一个,随后分析视频流的相关解码数据对视频进行解码。
- 读取mp4文件,将相关数据保存在封装上下文中;
- 从封装上下文中扒拉出来一个视频流出来(判断流的编码类型是不是视频);
- 通过视频流中.codecPar参数创建对应的解码上下文、解码器,最后通过
avcodec_open2()
进行加载; - 创建输出文件;
- 创建frame和packet;
- 从封装上下文中读取一个个packet,如果读到的话:
- 将从解码器上下文中扒拉出来完成解码的一帧,如果成功的话
- 分别将对应通道的裸数据写到输出文件中;
- 向解码器发送新的packet;
- 释放packet;
- 将从解码器上下文中扒拉出来完成解码的一帧,如果成功的话
- 关闭输出文件;
- 释放帧;
- 释放封装和解码上下文;
2.2 代码
|
|
3 尾巴
从裸数据到mp4文件和mp4到裸数据这两个步骤来看,视频的编码解码封装解封装的核心也就是下面这几步:
从来源看:
- 解码阶段的packet是从封装上下文formatctx中获取的;
- 编码阶段的packet是从编码器那里获得的;
- 解码阶段的frame是从解码上下文CodecCtx中获得的;
- 编码阶段的frame是从裸数据得到的;
从去向看:
- 解码阶段的packet会发送给解码器进行解码;
- 编码阶段的packet会发送给编码上下文;
- 解码阶段的frame会被写到原始文件中;
- 编码阶段的frame会发送给编码器;
从解码流程看:
- 拿到封装上下文;
- 从封装上下文拿到packet;
- packet发给解码器得到frame;
- frame信息写到裸数据中;
从编码流程看:
- 从裸数据得到frame数据;
- frame数据发给编码器得到packet;
- packet发送给封装上下文;
- 封装上下文写一个所需的封装格式;