Alby's blog

世上没有巧合,只有巧合的假象。

0%

FFmpeg 与 VideoToolBox(2):软解 H.264

一、概述

解码从数据结构上看,是将 AVPacket 转换为 AVFrame 的过程。如何构建 AVPacket,以及获取到 AVFrame 后续要进行什么操作,严格来说并不是解码的步骤。
本文主要关注使用 FFmpeg 的软解 H.264。虽然本文并不涉及 VideoToolBox,但硬解和软解有一些共用的 API,并且软解过程更流畅,所以熟悉软解后再去了解硬解会容易。

二、标准格式视频文件的解码

1、解码过程描述

以下内容来自雷霄骅博客:
视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。他们的过程如图所示。

01

视频播放步骤

解协议的作用,就是将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。

解封装的作用,就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。

解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。

视音频同步的作用,就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2、解码过程对应的 FFmpeg API

解码过程对应的 FFmpeg API,可以用伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
avformat_open_input() // 解协议。打开流并读取头,从而获取一个格式上下文 AVFormatContext 对象。
av_find_stream_info() // 解封装
av_find_best_stream() // 读取一定数据获取元数据从而能够初始化一个解码器 AVCodec 对象。也可以是使用 avcodec_find_decoder 手动获取解码器。
avcodec_alloc_context3() // 初始化并返回一个解码器上下文 AVCodecContext 对象。
avcodec_parameters_to_context() // 给 AVCodecContext 补充一些必要的参数。一般可从 AVFormatContext 的对应的 stream 的 codecpar 获取这些参数。
while(av_read_frame()) // 逐帧读取,返回的是 AVPacket。对于视频,如果读取成功,1 Packet 总数包含 1 Frame,对于音频暂时不表。
{
// 解码操作分成两个函数调用
avcodec_send_packet() // 将包含 1 Frame 数据 1 Packet 传入解码器
avcodec_receive_frame() // 解码回调,返回 AVFrame。格式是 yuv420p 。
}
// 资源释放操作

备注:

  1. 不使用 av_read_frame 函数,而是采用标准 I/O 的方式读取数据也是可行的,可使用 av_parser_parse2 函数来封装 AVPacket 。
  2. 本文说的”返回”不一定指函数的返回值,也可能是通过指针参数传入,由函数填充的数据。如果是双重指针,则一般是函数进行内存分配。

3、注意事项

如果解码 mp4 时在调用 avcodec_send_packet 函数失败,确保调用了 avcodec_parameters_to_context 函数。在官方示例 decode_video 的 avcodec_alloc_context3 调用之后也有个注释:

1
2
3
4
5
6
7
8
9
c = avcodec_alloc_context3(codec);
if (!c) {
fprintf(stderr, "Could not allocate video codec context\n");
exit(1);
}

/* For some codecs, such as msmpeg4 and mpeg4, width and height
MUST be initialized there because this information is not
available in the bitstream. */

因为可能存在 1 Packet 多 Frame 的情况,所以一次 avcodec_send_packet 通常配合多次 avcodec_receive_frame调用。官方示例 decode_video:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt,
const char *filename)
{
char buf[1024];
int ret;

ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
fprintf(stderr, "Error sending a packet for decoding\n");
exit(1);
}

// 循环调用 avcodec_receive_frame
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during decoding\n");
exit(1);
}

printf("saving frame %3d\n", dec_ctx->frame_number);
fflush(stdout);

/* the picture is allocated by the decoder. no need to
free it */
snprintf(buf, sizeof(buf), "%s-%d", filename, dec_ctx->frame_number);
pgm_save(frame->data[0], frame->linesize[0],
frame->width, frame->height, buf);
}
}

三、自定义格式视频文件的解码

1. 解码过程描述

因为不是标准格式,所以上文中的一些解码过程在这里是不必要的。以《FFmpeg 与 VideoToolBox(1):准备工作》准备的 temp.data 这样的文件为例:
(1) 非标准的;
(2) 有封装格式又足够简单的,每一帧数据有个长度头(4字节);
(3) 知道文件包含的是 H.264 数据;
(4) SPS、PPS 数据已经在 IDR 帧中。
解协议、解封装是不必要的;SPS、PPS已经在帧数据里面,不用调用 avcodec_parameters_to_context ;采用 av_find_best_stream 去初始化解码器 AVCodec 对象是不可能的; av_read_frame 可用标准 I/O 函数替代。

2. 解码过程对应的 FFmpeg API

解码过程对应的 FFmpeg API,可以用伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fopen() // 打开文件
avcodec_find_decoder(AV_CODEC_ID_H264) // 以 AV_CODEC_ID_H264 初始化并返回解码器 AVCodec
avcodec_alloc_context3() // 初始化并返回一个解码器上下文 AVCodecContext 对象。
while(true) // 逐帧读取,返回的是 AVPacket。对于视频,如果读取成功,1 Packet 总数包含 1 Frame,对于音频暂时不表。
{
fread() // 读取4字节的帧头
fread() // 根据帧头标示的帧长度读取帧
// 将帧数据放入一个 AVPacket 结构中
avcodec_send_packet() // 将包含 1 Frame 数据 1 Packet 传入解码器
while(true)
{
avcodec_receive_frame() // 返回 AVFrame。格式是 yuv420p 。
}
}
// 传入 null 的 AVPacket 再次调用 avcodec_send_packet,以确保解码器中的数据全部取出
avcodec_send_packet()
while(true)
{
avcodec_receive_frame() // 循环调用该函数
}

// 资源释放操作

参考资料