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

推荐订阅源

A
Arctic Wolf
博客园 - 聂微东
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
小众软件
小众软件
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Cloudflare Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园_首页
L
LangChain Blog
A
About on SuperTechFans
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
T
The Blog of Author Tim Ferriss
Security Latest
Security Latest
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
S
Secure Thoughts
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
P
Privacy & Cybersecurity Law Blog
O
OpenAI News
H
Heimdal Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Help Net Security
Help Net Security
C
Cyber Attacks, Cyber Crime and Cyber Security
Blog — PlanetScale
Blog — PlanetScale
GbyAI
GbyAI
G
Google Developers Blog
博客园 - Franky
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
Kaspersky official blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tor Project blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tenable Blog
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives

豆沙工作室

停止 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:04 - 举起渲染器 PreFound:03 - 跨越比特面 PreFound:01 - 新建文件夹 PreFound:00 - 梦开始的地方 向着无穷远跑去 如何打包一个 Python 项目 只能作为光盘手册存在的书籍 航天飞机和马屁股的关系 Get an iPod 从鸿蒙系统中拉取蓝牙 HCI 日志 第二类擦除 迁移 FreshRSS 的数据库
PreFound:02 - 启动编码器
https://archive.dsstudio.tech/person/ · 2026-05-24 · via 豆沙工作室

当心:前方有龙。

由于我们是直接在内存中渲染图形并喂给 FFmpeg 进行编码的,我们需要自行控制编码器、编码流和容器写出。FFmpeg 原生提供的用于操作文件的帮助函数我们的使用就相对少一些。

查找系统中可用的编码器

上一篇中我们注意到了虽然 FFmpeg 输出它支持的编码器很多,但系统上并不是所有的编码器都装了的。现在,我们需要筛选出实际可以用的编码器。具体的,我们关心系统上是否存在一个可以进行 H.264 编码的编码器:

extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libavutil/opt.h"
}

#include "spdlog/spdlog.h"

bool select_video_encoder(char* buffer, size_t buffer_size) {
    const AVCodec* codec;
    void* handle = nullptr;

    while ((codec = av_codec_iterate(&handle))) {
        if (!av_codec_is_encoder(codec) || codec->type != AVMEDIA_TYPE_VIDEO) {
            continue;
        }

        spdlog::debug(
            "known video codec: {} ({})", codec->name, codec->long_name);

        // find h264
        if (strstr(codec->long_name, "H.264") || strstr(codec->name, "h264")) {
            spdlog::info(
                "selected video codec: {} ({})", codec->name, codec->long_name);
            strncpy(buffer, codec->name, buffer_size);
            return true;
        }
    }

    spdlog::warn("no suitable video encoder found");
    return false;
}

你的系统可能这个函数确实会返回 false. 但是没关系,可以仿照查找 H.264 编码器的方法继续查找其他编码器(如 mpeg4, vp9 或者 av1)。如果确实是一个都找不着……那可能确实需要开一些 GPL 库才可以。

在 macOS 下,h264 编码由 h264_videotoolbox 实现。这个编码器是默认 FFmpeg 配置下就可用的。

还有一个方法是使用 avcodec_find_encoder 方法来查找注册到系统中的编码器,比如:

const auto* codec = avcodec_find_encoder(AV_CODEC_ID_H264);

如果对应的 ID 下没有可用的编码器,则该函数返回 nullptr. 我们可以通过传入可接受的 AV_CODEC_ID_* 来逐个查找系统中可能的编码器实现,此处不再赘述。

后文中,我将假定所使用的编码器实现 H.264 编码格式,或者至少支持 YUV420 格式输入。

启动编码器

首先,根据查找到的可用编码器名,召唤对应的编码器实现:

// include 和函数签名略,就当我们在 main 函数里
int err = 0;
char codecName[64];
if (!select_video_encoder(codecName, sizeof(codecName))) {
    spdlog::error("no codec available");
    return -1;
}

const auto* codec = avcodec_find_encoder_by_name(codecName);
if (!codec) {
    spdlog::error("cannot find codec `{}`", codecName);
    return -2;
}

接下来需要申请一个编码器上下文:

auto* context = avcodec_alloc_context3(codec);
if (!context) {
    spdlog::error("failed to allocate codec context");
    return -3;
}

编码器上下文用于保存编码器配置,比如比特率、帧率等等信息。假设我们需要输出的视频文件尺寸为 640x480@60fps, 码率为 10Mbps, 则可以这样设置编码器上下文:

context->bit_rate = 10000000;
context->width = 640;
context->height = 480;
context->time_base = (AVRational){1, 60};
context->framerate = (AVRational){60, 1};

一般来说,大部分编码器都需要 YUV420 格式、逐行扫描的输入:

context->pix_fmt = AV_PIX_FMT_YUV420P;

对于 H.264 这种带有帧间压缩的编码,则同样需要指定帧间压缩参数。比如我们需要每 10 帧生成一个 I 帧,则可以这样配置:

context->gop_size = 10;

以及,关闭 B 帧生成。这主要是因为如果有 B 帧,后续将 H.264 码流导入视频容器时会需要处理各种比较棘手的情况。

context->max_b_frames = 0;

对于 H.264 编码,我们还可以设置其使用高质量(缓慢)编码预设:

if (codec->id == AV_CODEC_ID_H264) {
    av_opt_set(context->priv_data, "preset", "slow", 0);
}

完成上述设置之后,我们就可以尝试启动编码器了:

err = avcodec_open2(context, codec, nullptr);
if (err < 0) {
    spdlog::error("failed to open the codec: {}", err);
    return -4;
}

设置基本数据结构

创建一个用于承载视频信息的帧:

auto* frame = av_frame_alloc();
if (!frame) {
    spdlog::error("failed to allocate a frame");
    avcodec_free_context(&context);
    return -5;
}

以及一个用于承载编码信息的包:

auto* packet = av_packet_alloc();
if (!packet) {
    spdlog::error("failed to allocate a packet");
    avcodec_free_context(&context);
    av_frame_free(&frame);
    return -6;
}

这两个数据结构在整个编码循环中都可以重复使用,并不需要每个视频帧或者每次编码都创建。

另一个有趣的地方是 FFmpeg 的许多清理函数都是传入 T** 而非 T* 的。在对应的变量被清理后,FFmpeg 会自动将其内容设置为 nullptr.

我们同样需要设置帧的大小信息,复用之前的视频信息配置即可:

frame->format = context->pix_fmt;
frame->width = context->width;
frame->height = context->height;

之后,即可分配帧所需要使用的缓冲区了:

err = av_frame_get_buffer(frame, 0);
if (err < 0) {
    spdlog::error("failed to allocate frame buffer: {}", err);
    avcodec_free_context(&context);
    av_frame_free(&frame);
    av_packet_free(&packet);
    return -7;
}

当然,我们会需要保存编码结果。目前我们先打开一个文件用于保存编码结果:

FILE *fp = fopen("output.h264", "wb");
// 错误处理从略

编码循环

接下来,我们将生成 300 帧(5 秒钟)的视频数据并编码。

for (int i = 0; i < 300; i++) {
    // 接下来标记为 in for-loop 的代码发生在这里
}

首先,和其他图形操作一样,我们需要先让帧缓冲区进入可写状态:

// in for-loop
err = av_frame_make_writable(frame);
if (err < 0) {
    spdlog::error("cannot make frame writable: {}", err);
    break;
}

之后,我们就可以操作帧缓冲区中的数据了。如果我们已经有生成好的图形数据,就是在这个步骤填入的。

YUV420 是一种平坦格式,即它的数据是按 YYYY...CbCbCbCb...CrCrCrCr... 的顺序存储的。相比而言,RGB888 则是一种交错格式,即它的数据是按 RGBRGBRGBRGB... 的顺序存储的。

我们在这里先生成一些测试用的数据:

// in for-loop
// Y 分量的数据
for (int y = 0; y < context->height; y++) {
    for (int x = 0; x < context->width; x++) {
        frame->data[0][y * frame->linesize[0] + x] = (x + y + i * 3) & 0xff;
    }
}

// CbCr 分量的数据
// 注意到 YUV420 中,色度数据量是明度数据量的一半
for (int y = 0; y < context->height / 2; y++) {
    for (int x = 0; x < context->width / 2; x++) {
        frame->data[1][y * frame->linesize[1] + x] = (128 + y + i * 2) & 0xff;
        frame->data[2][y * frame->linesize[2] + x] = (64 + x + i * 5) & 0xff;
    }
}

// 指定这个是第几帧
frame->pts = i;

完成帧数据的填充后,就可以提交到编码器进行编码了:

// in for-loop
err = avcodec_send_frame(context, frame);
if (err < 0) {
    spdlog::error("failed to send the frame to encoder: {}", err);
    break;
}

提交到编码器后,就可以从编码器中取出编码数据并写出文件:

// in for-loop
while (err >= 0) {
    err = avcodec_receive_packet(context, packet);
    if (err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
        // 继续编码下一帧
        break;
    }

    if (err < 0) {
        spdlog::error("failed to get packet from encoder: {}", err);
        break;
    }

    fwrite(packet->data, 1, packet->size, fp);
    // 包数据操作完成,释放包内的数据引用
    av_packet_unref(packet);
}

结束编码

完成所有的视频信息编码后,我们还需要清理一下编码器的状态,并写出编码结尾:

err = avcodec_send_frame(context, nullptr);
if (err) {
    // 错误处理从略
}

while (err >= 0) {
    err = avcodec_receive_packet(context, packet);
    if (err == AVERROR(EAGAIN) || err == AVERROR_EOF) {
        // 继续编码下一帧
        break;
    }

    if (err < 0) {
        spdlog::error("failed to get packet from encoder: {}", err);
        break;
    }

    fwrite(packet->data, 1, packet->size, fp);
    // 包数据操作完成,释放包内的数据引用
    av_packet_unref(packet);
}

然后关闭文件和各个使用到的数据结构:

fclose(fp);
avcodec_free_context(&context);
av_frame_free(&frame);
av_packet_free(&packet);

成果展示……?

真是绕了好大一圈啊!但你可能发现,我们输出的文件名并不是 output.mp4, 而是 output.h264. 输出的文件似乎也没法直接双击播放。如果使用 file 命令查看文件类型的话,会发现它并不是我们熟悉的文件格式:

output.h264: JVT NAL sequence, H.264 video @ L 30

通过 ffplay 程序,我们还是可以把图像放出来的。执行 ffplay output.h264, 可以看到形似图 1 的画面:

output.h264.png

图 1 output.h264

但是 ffplay 输出的是 25 帧,和我们指定的 60 帧并不一样。

要解决这个问题,我们需要将编码器的输出灌到一个视频容器中。

创建容器对象

在启动编码器之前,我们需要先准备好操作视频容器所需要的数据结构。

这里我们使用 Matroska 容器作为视频容器对象:

AVFormatContext* format = nullptr;
err = avformat_alloc_output_context2(&format, nullptr, "matroska", nullptr);
if (err < 0) {
    spdlog::error("failed to allocate output context: {}", err);
    // ...
    return -10;
}

一个视频容器中,各个组成部分的数据由对应的流表示。我们创建一个流用于承载视频流信息:

auto* stream = avformat_new_stream(format, nullptr);
if (!stream) {
    spdlog::error("failed to allocate stream");
    // ...
    return -11;
}

stream->id = static_cast<int>(format->nb_streams - 1);

有了视频容器和流之后,我们就可以先打开文件了:

err = avio_open(&format->pb, "output.mkv", AVIO_FLAG_WRITE);
if (err < 0) {
    spdlog::error("failed to open output file: {}", err);
    // ...
    return -12;
}

由于 Matroska 需要从编码器中读取数据,我们需要给编码器头设置 AV_CODEC_FLAG_GLOBAL_HEADER 以便元数据可可以取到:

context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
err = avcodec_open2(context, codec, nullptr);
if (err < 0) {
    spdlog::error("failed to open the codec: {}", err);
    return -4;
}

启动编码器后,我们将编码器信息复制给流,以便它能正确地写出对应的元信息:

err = avcodec_parameters_from_context(stream->codecpar, context);
if (err < 0) {
    spdlog::error("failed to copy codec context: {}", err);
    return -13;
}

完成上述设置之后,即可先写出视频容器的头信息:

err = avformat_write_header(format, nullptr);
if (err < 0) {
    spdlog::error("failed to write header: {}", err);
    return -14;
}

在编码循环和结尾中,写文件操作现在由写流替换:

av_packet_rescale_ts(packet, context->time_base, stream->time_base);
packet->stream_index = stream->index;
av_interleaved_write_frame(format, packet);
av_packet_unref(packet);

结尾也要让视频容器写出尾部信息:

av_write_trailer(format);
avformat_flush(format);

并加入对应数据结构的清理:

avio_close(format->pb);
avformat_free_context(format);

完整的代码可以在这里获取。

成果展示!

这回生成的 output.mkv 文件终于是可以正常播放了:

output.mkv.png

图 2 output.mkv

更多的问题

你可能注意控制台会输出一行并不来自我们的警告:

[h264_videotoolbox @ 0x715010000] Color range not set for yuv420p. Using MPEG range.

以及,我们如果用程序生成图像的话,一般使用的是 RGB 格式;但现在写视频帧却需要使用 YUV420 格式。这个格式转换需要我们手动进行么?

今天已经烧够好多个三分钟了,这些问题之后再考虑吧!