Alby's blog

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

0%

FFmpeg 与 VideoToolBox(4):x264 软编 H.264 和 VideoToolBox 硬编 H.264

一、概述

编码从数据结构上看,是将 AVFrame 转换为 AVPacket 的过程。如何构建 AVFrame,以及获取到 AVPacket 后续要进行什么操作,严格来说并不是编码的步骤。
x264 是作为一个插件编译入 FFmpeg 的。 FFmpeg 对 VideoToolBox 也做了适配。单独使用 x264VideoToolBox 也可以进行编码,本文主要关注通过 FFmpeg 相关 API 进行 H.264 编码。

二、视频文件准备工作

x264 支持多种像素格式:

1
2
3
4
5
$ ffmpeg -hide_banner -h encoder=libx264
Encoder libx264 [libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10]:
General capabilities: delay threads
Threading capabilities: auto
Supported pixel formats: yuv420p yuvj420p yuv422p yuvj422p yuv444p yuvj444p nv12 nv16 nv21

h264_videotoolbox 支持的像素格式要少些:

1
2
3
4
5
$ ffmpeg -hide_banner -h encoder=h264_videotoolbox
Encoder h264_videotoolbox [VideoToolbox H.264 Encoder]:
General capabilities: delay
Threading capabilities: none
Supported pixel formats: videotoolbox_vld nv12 yuv420p

准备一个很常用也很容易获得的 yuv420p 格式的视频文件。
FFmpeg 内置解码器解码出来的 AVFrame 里面的数据就是 yuv420p,不过 YUV 数组是分放在 3 个数组中的,可以拷贝至一个新建的 Buffer 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AVFrame *frame = av_frame_alloc();
// 解码操作
int buffer_size = frame->height * frame->width * 1.5;
uint8_t *buffer = malloc(buffer_size);

int offset = 0, i;
for (i = 0; i < height; i++)
{
memcpy(buffer + offset, frame->data[0] + i * frame->linesize[0], width);
offset += width;
}
for (i = 0; i < height / 2; i++)
{
memcpy(buffer + offset, frame->data[1] + i * frame->linesize[1], width / 2);
offset += width / 2;
}
for (i = 0; i < height / 2; i++)
{
memcpy(buffer + offset, frame->data[2] + i * frame->linesize[2], width / 2);
offset += width / 2;
}

备注:

  1. 如果解码出来是 nv12 之类的,将数据写入 Buffer 的算法是不同的,这里不详述。
  2. 官方示例 decode_video 保存的是 pgm 格式的文件。

三、编码过程描述

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

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

fopen() // 打开文件
avcodec_find_encoder_by_name("libx264") // 获取解码器 AVCodec。也可使用 avcodec_find_encoder(AV_CODEC_ID_H264)
avcodec_alloc_context3() // 初始化并返回一个解码器上下文 AVCodecContext 对象。
// 设置 AVCodecContext 的 bit_rate、width、height、time_base、framerate、gop_size、max_b_frames 和 pix_fmt 等,以及使用 av_opt_set 函数设置 priv_data 的 profile、level、preset 和 tune 等选项。
av_frame_alloc() // 创建一个可以复用的 AVFrame
// 设置 AVFrame 的 format、width 和 height
av_frame_get_buffer() // 给 AVFrame 分配空间 (注:按 32 字节对齐)
av_frame_make_writable() // 使 AVFrame 可写
avcodec_open2() // 打开编码器
while(true) // 逐帧读取,返回的是 AVFrame。对于视频,如果读取成功,1 Packet 总数包含 1 Frame,对于音频暂时不表。
{
fread() // 读取4字节的帧头
fread() // 根据帧头标示的帧长度读取帧
// 将 YUV 数据分别复制入 AVFrame 结构的 data[0]、data[1] data[2] 中
// 设置 AVFrame 的 pts。按 1 递增即可。
avcodec_send_frame() // 将包含 1 Frame 数据 1 AVFrame 传入解码器
while(true)
{
avcodec_receive_packet() // 返回 AVPackget
}
}

// 传入 null 的 AVFrame 再次调用 avcodec_send_frame,以确保编码器中的数据全部取出
avcodec_send_frame()
while(true)
{
avcodec_receive_packet()
}

// 资源释放操作

四、使用 VideoToolBox 编码

使用 VideoToolBox 编码非常简单,将 avcodec_find_encoder_by_name("libx264") 中的参数改为 h264_videotoolbox 即可。

备注:

  1. 在官方的示例 encode_video.c 中使用 VideoToolBox 时,请将分辨率改为 1280 * 720 等改为 VideoToolBox 支持的分辨率。
  2. 不设置 AVCodecContexthw_device_ctx 似乎也不影响编码。

参考资料