FFmpeg 解码视频流
FFmpeg 解码视频流
前言
本文讲解从网络接收 H.265 视频裸流后使用 FFmpeg 相关库解码得到 YUVJ420P 格式的图片然后转为 RGB 格式图片并使用 jpeglib 库保存为 jpeg 文件。仅展示部分代码。
包含对 FFmpeg 的解码 H.265 视频裸流、图片保存格式 YUV 和 RGB 及两者之间的转换、jpeglib 将 RGB 格式图片保存为 jpeg 文件的相关内容。
FFmpeg 库
包含各种库,本文主要使用到以下库:
libavcodec 编码/解码库
libavformat I/O 和复用/解复用库
libavutil 通用实用程序库
libswscale 颜色转换和缩放库
跟解码相关的结构体
AVCodecContext:可以是 编码器 的上下文,也可以是 解码器 的上下文,两者使用的是同一种数据结构(后面有些结构体和 API,编码器和解码器都可以使用,但为了方便,后面只提解码器)。AVCodec:存储解码器信息(为了方便,后面简称就是“解码器”了,基本上也可以这么理解)。AVCodecParameters:存储解码参数。AVPacket:原始数据包(已编码压缩的 IBP 帧),这里面的数据通常是一帧视频的数据,或者一帧音频的数据。本身是没有编码数据的,只是管理编码数据。AVFrame:解码之后的帧。AVFrame跟AVPacket类似,是一个管理数据的结构体,本身是没有数据的,只是引用了数据。
与解码器相关的 API 函数
const AVCodec* avcodec_find_decoder(enum AVCodecID id):根据参数传入的解码器的
AVCodecID返回指定的解码器。AVCodecID是一个枚举类型,比如:传入AV_CODEC_ID_H265返回解码 H265 视频流的解码器。AVCodecContext* avcodec_alloc_context3 (const AVCodec *codec):接收一个
AVCodec( 编解码)参数,返回根据指定解码器初始化后的解码器上下文。如果为 NULL,则不会初始化特定于解码器的默认值,这可能会导致默认设置不理想。使用
void avcodec_free_context (AVCodecContext **avctx )释放解码器上下文资源。AVCodecParameters *avcodec_parameters_alloc():分配新的 AVCodecParameters 并将其字段设置为默认值,返回的AVCodecParameters必须使用
avcodec_parameters_free()释放。int avcodec_parameters_from_context(struct AVCodecParameters *par,const AVCodecContext *codec)根据提供的
AVCodecContext中的值填充AVCodecParameters。par 中任何分配的字段都将被释放,并替换为解码器中相应字段的副本。成功时 >= 0,失败时为负 AVERROR 代码。int avcodec_parameters_to_context(AVCodecContext *codec, const struct AVCodecParameters *par):根据
AVCodecParameters参数提供的解码器参数(宽高,像素格式等信息)复制到AVCodecContext(解码器上下文)。int avcodec_open2( AVCodecContext *avctx, const AVCodec* codec, AVDictionary **options):使用初始化后
AVCodecContext(编解码器上下文)和设定的选项打开一个编/解码器。在使用此函数前,必须使用avcodec_alloc_context3()初始化上下文。int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt):往
AVCodecContext绑定的解码器发送一个AVPacket(需要被解码的一个原始数据帧)。帧的所有权仍属于调用者。返回0表示成功;返回AVERROR(EAGAIN)表示当前状态不接受输入,必须调用下面的函数读取输出,然后重新发送数据包;调用前必须用avcodec_open2()打开AVCodecContext。int avcodec_receive_frame(AVCcodecContext* avtx, AVFrame* frame):从
AVCodecContext绑定的解码器读取一个AVFrame(解码后的一个输出数据帧),返回值为0表示成功,返回一帧;为AVERROR(EAGAIN)表示输出不可用,需要继续输入(可能是B\P帧);为AVERROR_EOF表示解码器完全刷新,不会再有输出帧。
与 AVPacket 和 AVFrame 相关的函数
AVPacket *av_packet_alloc():创建一个 AVPacket 并将其字段设置为默认值,失败时返回 NULL。必须使用
av_packet_free()释放生成的结构。函数仅分配 AVPacket 本身,而不分配数据缓冲区。这些必须通过其他方式分配,例如使用av_new_packet。AVFrame *av_frame_alloc():创建一个 AVFrame 并将其字段设置为默认值。失败时返回 NULL。必须使用
av_frame_free()释放生成的结构。函数仅分配 AVFrame 本身,而不分配数据缓冲区。这些必须通过其他方式分配,例如使用av_frame_get_buffer()。void* av_malloc(size_t *size*)分配一个内存块,其对齐方式适合所有内存访问(包括 CPU 上可用的矢量)。类似于 malloc。主要用于为初始化后的
AVPacket和AVFrame分配 data 内存。int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size):从已经使用
av_malloc()分配内存的数据初始化引用计数的数据包。pkt指向要初始化的数据包,data为由av_malloc()分配的数据缓冲区。成功返回 0,数据归基础 AVBuffer 所有。调用方可能无法通过其他方式访问数据。
AVCodec 和 AVCodecParameters补充
AVCodec里面放的是 解码器的相关信息 。
AVCodec 是使用 avcodec_find_decoder() 函数获得的(见上文),传递个函数一个 AVCodecID,返回一个对应的解码器指针。这是引入 FFmpeg 库的时候,他初始化了一堆静态的编解码变量给你。
例如:传递的 AVCodecID 是 AV_CODEC_ID_H265 ,就会返回与 H265 视频相关的 AVCodec 指针, AVCodecID 是 AV_CODEC_ID_H264 ,就会返回与 264 视频相关的 AVCodec 指针。
只要是用 H264 编码的视频,使用的解码器都是一样的,用的是同一个 AVCodec。
AVCodecParameters里面放的是 解码参数。
虽然都是 H265 视频文件,但是他们的宽高,采样这些信息,可能会不一样,这些信息就存在AVCodecParameters里,
当 avformat_open_input 函数打开一个 MP4 的时候,解码参数就会放在 codecpar 字段里,如下:
int avformat_open_input(AVFormatContext** ps, const char* url, const AVInputFormat* fmt, AVDictionary** options)
但是如果直接获得的裸流,比如本问所描述的,没有文件供打开而获得 AVCodecParameters,因此需要使用AVCodecParameters *avcodec_parameters_alloc()和int avcodec_parameters_from_context(struct AVCodecParameters *par,const AVCodecContext *codec)来构建 AVCodecParameters 。
例如:ps 为 NULL 时,自动分配 AVFormatContext 并将其写入 ps 中,url 可以是网络 URL 也可以是本地文件名;fmt为 NULL 时,此参数强制使用个特定的输入格式,不为 NULL 时自动检测格式;options 参数被销毁,并替换为包含未找到的选项的字典,可能为 NULL。例如:
1 | avformat_open_input(&fmt_ctx, filename, NULL,NULL); |
上面的 codecpar 就是一个 AVCodecParameters,只需要用 avcodec_parameters_to_context 函数把 codecpar 的参数复制给 AVCodecContext 即可。
解码流程
打开解码器的一般流程
avcodec_alloc_context3() ➔ avcodec_parameters_to_context() ➔ avcodec_find_decoder() ➔ avcodec_open2()
avcodec_alloc_context3 跟 avcodec_open2 这两个函数都可以接受 AVCodec 参数,选一个函数来接受即可,千万不要往这两个函数传递不一样的 AVCodec 参数。
avcodec_alloc_context3() 传入 NULL,初始化一个解码器上下文。
avcodec_parameters_to_context()将已有的解码参数复制到解码器上下文。
avcodec_find_decoder()根据传入 ID 获得对应的解码器,根据传传入的解码器上下文和解码器信息打开解码器。
示例代码部分如下(为了简短均为没有检测返回值,实际应用时应该检测返回值):
1 | AVFormatContext *fmt_ctx = NULL; |
H.265视频解码流程
首先,我们已经从网络接收到了待处理的一个个原始视频帧的缓冲区,里面是 H.265 视频裸流。这里注意,裸流可能不包含前四个字节:0x00,0x00,0x00,0x01,这四个字节是区分一帧的关键必须包含,如果没有就手动添加到packet->data开头,否则交由解码器解码时会报错。
开启一个发数据包的循环不断的将其作为 AVPacket 发给解码器。
解码器收到一个 AVPacket后,我们不一定立马就能从解码器读到一个 AVFrame,因为视频可能有 B 帧,一个单独的 B 帧无法得到一个完整的帧。需要继续给解码器提供后续的 AVPacket 才能得到一个完整的帧。同时在我们提供一个 AVPacket 后,也可能会得到两个完整的帧(由于 B 帧的存在)。
因此,我们发一个 AVPacket后,再开启一个循环读解码器,如果返回值为0,就说明得到一帧 YUV 信息,进行一些处理。如果返回值为EAGAIN,它表示解码器需要输入更多的 AVPacket ,才能解码出 AVFrame,我们就跳出第读解码器的循环,进入发 AVPacket 的循环继续发 AVPacket 。
当所有的 AVPacket 都已经发完时,解码器还可能在等待后续的 AVPacket 到来以解码之前收到的 B 帧,但此时我们已经没有 AVPacket 可以发送给解码器了。此时需要往解码器发一个 size 跟 data 都是 0 的 AVPacket(NULL)。这样解码器就会把它内部剩余的帧,全部都刷出来。
当解码器完全没有帧可以输出的时候,就会返回 AVERROR_EOF。
解码后得到的 AVFrame
部分字段:
frame->width,frame->height:帧的宽高。frame->pts:帧的显示时间点frame->pkt_duration:帧的持续时间frame->format:帧的格式,有很多,整数枚举类型,0 代表AV_PIX_FMT_YUV420P。frame->key_frame:(已弃用,使用 flags 中的AV_FRAME_FLAG_KEY代替)代表当前帧是不是关键帧,第一帧通常都是关键帧。I 帧为关键帧,可以单独解码出一个AVFrame,B、P 帧不是关键帧,不能单独解码出一个AVFrame。frame->pict_type:这个是AVPictureType枚举类型,1 代表AV_PICTURE_TYPE_I,即 I 帧。frame->data:存储帧的 Y、U、V 信息。
解码的到的一般是 YUV420P 格式即上面frame->format的值:AV_PIX_FMT_YUV420P。它是一个存储帧的格式,以一定的方式存储 Y、U、V 信息(这方面展开说有些多,这里只看 YUV420P ):
Y 表示一帧的一块区域的明亮度,U 和 V 表示这块区域的色度与浓度。每个 Y U V 在内存中都占8位。
420 表示4:2:0采样,一个像素一个 Y,水平和垂直方向上都是每两个像素采一个 U 和 V。即每四个Y共用一组 UV 分量。内存上 Y 是 UV 大小的四倍。
P 表示Planar,YUV 按平面分开放,三个平面依次存放,YUV都是连续存储的:

AVFrame 结构体里面有一个 data 字段,存储了 YUV 数据, YUV420P 使用 planar 格式的内存布局来存储,YUV420 使用 packed 格式的内存布局来存储,这里以 YUV420P 为例:
data[0] 会指向 Y 数据,data[1] 指向 U 数据,data[2] 指向 V 数据。还有一个 linesize 数组字段来管理这些数据的大小。例如linesize[0]表示data[0]指向的 Y 数据的一行的大小。YUV420P 的格式,U 或者 V 的大小应该是 Y的 4 分之一。
如果直接打印出 linesize 的值,会发现 U、V 的值是 Y 的二分之一。这是因为linesize 里存的是 stride 值。
stride 值 = 图像宽度 * 分量数 * 单位样本宽度 / 水平子采样因子 / 8
分量数就是通道数,YUV420P 是 planer 内存布局 ,所以分量数是 1。单位样本宽度是指一个样本的宽度占多少位,YUV420P 为 16 位。
最重要的是水平采样因子,水平子采样因子指在水平方向上每多少个像素采样出一个色度样本。YUV420P 水平方向其实是每两个像素采样一个色度样本,所以是水平采样因子是 2。
还有一个垂直采样因子,YUV420P 的 垂直采样因子 也是 2 。但是 垂直采样因子不会影响 stride 值。
frame->linesize 并不是 UV 分量的真实数据大小,而是一个 stride 值。并且从上面可以看出,如果使用的是 YUV420P 格式,会有:stride 值 = width * 1 * 16 / 2 / 8 = width,实际上就是帧的宽度 width。最终这个值可能还会内存对齐。
YUV420P 格式的帧保存为图片
通过data字段我们就可以得到帧的 Y、U、V 信息。 通过 linesize 字段我们可以的到他们的大小。但是一般情况下是data字段都是有内存对齐现象存在的。如果linesize 里存的 stride 值大小不足2的n次方,则会填充多余数据以达到2的n次方。例如,计算的到的linesize[0]为480,而它实际大小确是512。因此我们需要镜多余的数据剔除。
1 |
|
到这里,已经将 H265 裸流保存为了 YUV420P 的文件。





