惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
Martin Fowler
Martin Fowler
B
Blog
The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
DataBreaches.Net
月光博客
月光博客
人人都是产品经理
人人都是产品经理
IT之家
IT之家
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
博客园 - Franky
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
C
Check Point Blog
罗磊的独立博客

豆沙工作室

不想(二)十七岁 小点阵字 Get an Oscilloscope 杰理 WTS 格式音频转换和打包 [11ty] 增量式地刷新 CDN 缓存 站起来,为你的成果答辩吧 公理还是定理? [11ty] 处理资产文件的 CDN 缓存问题 [11ty] 无障碍设计:正确地标识图和表 停止 Mac 上的时间机器备份 从 Gitea 迁移到 Forgejo 评论里的私信消息 记得清理你的日志文件 招魂 Annihilation [11ty] 使用 Pagefind 实现静态站点的搜索 直面死亡 [11ty] 修复 RSS 里的数学公式 看到了一个神奇的 UA PreFound:10B - 四次击键 一点小小的 AI 震撼 PreFound:10A - 文本之外,还有(绘)文字 PreFound:09 - 色彩运算子 计算机不予申辩 Re-Game 2.0 Addendum C - Press START PreFound:08 - 动效与仿射 PreFound:07 - 图片拼贴报 PreFound:06 - 漫游雪花海 PreFound:05 - 写出到屏幕 And Trek I Will
PreFound:03 - 跨越比特面
https://archive.dsstudio.tech/person/ · 2026-05-24 · via 豆沙工作室

开始千里之行的第二个三分钟。

刚刚上期我们完成了基本的渲染循环的实现,留了两个关键问题:那个警告是从哪来的?以及怎么把 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;

完整的代码在这里

成果展示

output.mkv.png

图 1 output.mkv

下一步

经过这一段的折腾,我们终于搭建好了视频渲染的基础设施。接下来就是想办法开始程序化地填充 RGB Buffer 了。

程序化填充 RGB Buffer 的方法有很多,比如通过图形库即可实现无头渲染;或者如果我们确实想搞一点非常有趣的效果的话,也可以使用 OpenGL 的无窗口模式进行绘制。