开始千里之行的第二个三分钟。
刚刚上期我们完成了基本的渲染循环的实现,留了两个关键问题:那个警告是从哪来的?以及怎么把 RGB 转换成 YUV420?
指定色域
警告本身的内容很简单,因为我们没有指定色域,所以它默认使用了 MPEG 色域。
如果只是需要去掉这一行警告的话,在设置编码器上下文时明确指定色域即可:
context->color_range = AVCOL_RANGE_MPEG;
比较有趣的是:这个色域是一个窄色域,其明度范围为 [16, 235], 色度范围为 [16-240]. 另一个色域格式是 AVCOL_RANGE_JPEG, 其范围为 [0, 255].
虽然看似我们应该设置 AVCOL_RANGE_JPEG, 不过真这么做的话,会发现 VLC 等播放器中的颜色反而比设置为 AVCOL_RANGE_MPEG 要更为「黯淡」——尤其是后面我们开始从 RGB 图像中复制信息的时候,你会发现黑不够黑而白不够白。
和色彩相关的问题总是无穷无尽的,比如这玩意真的叫「色域」么?它和 Gamut 的区别是什么?为什么源码里叫它 MPEG/JPEG 而不是像其他程序中称其为 Limited/Full?
这些问题只能暂时先放下了。我们的目标是用代码程序化地生成视频,这些问题暂时不影响这个主要目标。
格式转换
尽管我们可以手动写 RGB 到 YUV420 的转换,但这块石头我还是不太想亲手搬的。FFmpeg 库为我们提供了 swscale 能力,我们可以直接借用。
分配一个 RGB 的缓冲区:
uint8_t *rgbBuffer = new uint8_t[context->width * context->height * 3];
以及分配一个 swscale 的上下文:
auto* swContext = sws_getContext(context->width,
context->height,
AV_PIX_FMT_RGB24,
context->width,
context->height,
context->pix_fmt,
0,
nullptr,
nullptr,
nullptr);
// 源和目标宽高一致;源格式为 RGB24, 目标格式遵循编码器设定,没有额外的 filter
有了这个上下文之后,我们就可以通过调用 sws_scale 函数来做格式之间的转换了。
先将之前写 YCbCr 空间的部分替换为写 RGB buffer:
// in for-loop
// after av_frame_make_writable
// 简单的条纹
for (int y = 0; y < context->height; y++) {
memset(&rgbBuffer[y * context->width * 3],
(y + i) & 0xff,
context->width * 3);
}
写完 RGB buffer 后,就可以直接通过 swscale 转换到 frame 当中去:
// in for-loop
const uint8_t* sourceDataPlanes[1] = {rgbBuffer};
const int sourceDataStrides[1] = {3 * context->width};
err = sws_scale(swContext,
sourceDataPlanes,
sourceDataStrides,
0,
context->height,
frame->data,
frame->linesize);
if (err < 0) {
spdlog::error("failed to convert frame data: {}", err);
continue;
}
当然,申请的资源要记得释放:
sws_freeContext(swContext);
delete[] rgbBuffer;
完整的代码在这里。
成果展示
图 1 output.mkv
下一步
经过这一段的折腾,我们终于搭建好了视频渲染的基础设施。接下来就是想办法开始程序化地填充 RGB Buffer 了。
程序化填充 RGB Buffer 的方法有很多,比如通过图形库即可实现无头渲染;或者如果我们确实想搞一点非常有趣的效果的话,也可以使用 OpenGL 的无窗口模式进行绘制。






















