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 的文件。