使用 fframes 制作视频
fframes 是一个用 Rust 编写的库,可以通过代码渲染视频。一个视频就是一个 Rust 结构体,它包含一个 render_frame(frame) -> Svgr 方法:为每一帧返回一个 SVG 树,该树使用 svgr! 宏(包含 {rust 表达式} 的 SVG 标记)构建。场景分割时间线,timeline! 和弹簧驱动动画值,音频映射布局声音,ffmpeg 编码最终结果。
每个项目都会获得一个命令行(来自 fframes::cli)。使用它来完成所有操作:渲染视频、打开实时预览窗口,以及查看帧、联系表和响度数值的文件。在开始设计之前,请阅读 references/design.md;编写代码时查阅 references/api.md;声音相关请参考 references/audio.md;3D 场景(fframes::three_d)请参考 references/3d.md。
当匹配提供的视频时,首先按照 references/design.md 中的参考分析指南进行操作。其剪辑、节奏和运动优先于以下设计默认值;快速剪辑和有意为之的黑帧可能是所请求结果的一部分。
在开始其他任何事情之前,有两条规则
先规划,再写代码
不要打开模板就开始制作动画。首先:
- 确定需求:谁在看视频以及在哪里看(平台决定格式和时长),观看者必须记住的一件事,以及现有的素材(Logo、产品截图、视频片段、音乐、画外音)。询问用户缺少哪些关键信息。切勿编造产品事实、数字或引用。
- 当视频需要配合音乐或人声剪辑时,先选择声音。其节奏、节拍和短语决定了其他所有内容的时机。
- 先规划动态设计,将动态设计与声音峰值同步,形成一个独特的流程:元素将从哪里出现,它们将如何变换。尝试为每个场景创建独特的动态设计,使其看起来独特且精致。视频不应看起来像幻灯片。
- 尝试思考观看最终视频的人的感受,并相应调整设计的每个部分:流程、节奏、节拍、动态和颜色。
拒绝“模板化”
“模板化”视频是由默认选择组成的:它们可以仅通过更改文本用于任何产品,观众在一秒内就能识别出来。除非用户要求,否则不要提供:
- 模板残留:起始布局、颜色、字体或装饰、占位符文本、虚假 Logo、编造的数字。
- 节拍轰炸:整个画面在每个节拍上缩放、抖动或闪烁。将小的元素(一个笔画、一个词、一个火花)与节拍同步,仅在少数几个剪辑点改变整个画面。
- 一成不变的运动:每个元素都以相同的缓动和持续时间向上淡入 40 像素。
- 静态场景:一个东西出现、保持并淡出,而其他所有东西都保持不动。
- 通用文案:“Introducing”、“Unleash”、“Seamless”、“Next-level”、三个词的标语。以真实人类说话的方式编写屏幕文本。
- 过多文本:文本量应该有限且切中要点。
相反,要给每个元素一个与主题相关的理由,并移除没有理由的元素。将精力放在细节和动态上,而不是文字上:次要运动、交错入场、绘制笔画、相互反应的对象、跨剪辑传递的元素。在向用户展示任何内容之前,查看一下帧条带,并思考如果只更改文本,这个视频是否仍然适用于不同的产品。如果是,那就更改创意,而不是颜色。
1. 安装
从 https://rustup.rs 安装 Rust,然后安装 ffmpeg 构建所需的系统库:
# macOS
brew install pkg-config ffmpeg x264 x265 opus nasm ninja
# Debian / Ubuntu
sudo apt-get install -y yasm nasm ffmpeg libx264-dev libx265-dev libopus-dev libclang-dev clang ninja-build libvpx-dev libasound2-dev
# Arch
sudo pacman -S ninja yasm nasm ffmpeg x264 x265 opus clang
Windows 使用预编译的 FFmpeg 9 共享构建(设置 FFMPEG_DIR 指向从 BtbN/FFmpeg-Builds 解压的 ffmpeg-n9.0-latest-win64-gpl-shared 构建,将其 bin 目录添加到 PATH,使用 winget install LLVM.LLVM 安装 LLVM,并设置 LIBCLANG_PATH)。详细信息在 fframes 的 README 中。
2. 创建项目
curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fframes/main/scripts/new-video.sh | bash -s -- my-video --yes
# 或者一次性安装生成器并直接使用:
cargo install --locked cargo-fframes
cargo fframes new my-video --format landscape --fps 30 --yes
选项:--template single-scene|multi-scene,--format landscape|portrait|square|uhd,--fps,--title,--backend,--dir。项目依赖于与已安装的 cargo-fframes 匹配的 fframes 版本;--git 改用仓库的 main 分支。始终传递 --yes 以避免等待输入。
模板:
single-scene(默认):单个场景。它适用于所有格式;从这里开始制作单镜头剪辑、标题卡和竖屏视频。multi-scene:两个场景。从这里开始制作包含多个场景的视频;它只接受landscape和uhd格式。
模板内容是展示 API 的占位符代码。请用专门为用户请求的视频而设计的布局、颜色、字体和装饰来替换。
使用 Skia GPU 后端(默认)。 cargo fframes new 会选择 Metal(macOS)或 Vulkan(Linux、Windows)上的 Skia。它的渲染速度比 CPU 后端快约 10 倍,并为你提供实时 preview 窗口。在 macOS 和 Linux 上,首次构建会下载预编译的 Skia 和 ffmpeg,并且只编译 Rust 依赖项(大约一分钟;其他目标,或同时编译 metal 和 vulkan,则会从源代码编译 Skia,耗时约 20 分钟)。立即开始构建,并在构建运行期间编写视频逻辑:
cd my-video && cargo build --release # 在后台运行,首次构建是最慢的
--backend cpu 不需要 Skia,但没有预览窗口;仅在确实没有 GPU 时选择它。
项目渲染后的结构如下:
my-video/
src/lib.rs # 视频(来自模板)
src/main.rs # 命令行
media/ # 编译到二进制文件中的字体、图像和音频(一个起始字体)
README.md # 下面的命令
以下所有命令都是 cargo run --release -- <command>。定义一个简短的 shell 函数(像 R="cargo run --release --" 这样的变量在 zsh(macOS 的默认 shell)中不会被分割成单词):
R() { cargo run --release -- "$@"; }
3. 循环工作流
使用此技能时,根本不要编写测试。 不要创建单元测试、集成测试、快照测试或视觉回归测试、测试模块或测试框架。使用 inspect、渲染的帧、联系表、音频检查和原生预览来验证视频。
优先预览,而非渲染。 如果视频使用 GPU 渲染后端并且此机器上有 GPU 可用,则运行原生预览(或向用户显示命令),而不是在每次更改后重新渲染视频。一旦更改准备好审查:
- 在可复制的 shell 代码块中显示完整的命令,使用实际的项目路径和包名;不要要求用户定义了
$R辅助工具。例如:cd /absolute/path/to/fframes && cargo run --release -p my-video -- preview 0s。 - 明确表示可以帮用户运行该预览。如果用户已授权打开或刷新预览,则显示命令并在该授权范围内继续。
在请求更改后,将命令的开始时间设置为更改部分之前或正好开始的时间,并留有足够的提前量以查看其进入或过渡。例如,preview 8.5s 允许用户审阅大约 9 秒处的更改,而无需重播开头部分。说明他们将看到哪个部分。当用户请求最终导出时,再渲染整个视频;如果该请求已经明确,则无需再次确认。
每次更改后:
$R timeline列出场景及其帧和秒范围,以及每个音轨。在查看像素之前,先在这里检查结构和节奏。$R inspect检查每 0.25 秒一帧以及每个场景的第一帧和最后一帧。它会报告缺失的图像、字体或字形、被画布边缘裁剪的文本、无效的 SVG(零尺寸矩形、不良半径)、损坏的变换和 panic,每个问题都带有时间和场景。出现错误时,它以退出码 2 退出。修复它报告的所有问题。仅出现在场景前几帧的警告通常是入场动画;使用帧条带进行检查。$R strip <scene or range> -n 12写入一个联系表(strip.png):在一张带标签的图像中显示均匀分布的帧。使用你的图像查看工具打开它。这是判断布局、节奏和运动最快的方法。$R strip 2s,5s,9s将所选时间并排放置。$R frame Intro@end,Outro@50%将全尺寸 PNG 写入frames/目录,用于细节检查(排版、对齐、对比度),并打印在这些帧中发现的问题。在锐利的放大裁剪图上判断小细节(元素之间的接触、边缘、小形状),切勿在缩小后的帧上判断:$R frame 3s --crop 1100,520,240,120(视频像素,放大到约 960 像素宽)。--ids用于勾勒并命名每个具有id的元素,以识别哪个形状是哪个。$R onion "[email protected]@1s" -n 6将多帧混合成一张图像(onion.png),以显示运动的路径和间距:缓动、过冲、交错。$R preview Intro打开一个带有声音的实时窗口供用户观看(空格 播放/暂停,h/l 前进/后退一秒,j/k 步进一帧,q 退出)。它会阻塞直到窗口关闭,因此在为用户运行时请在后台启动它。如上所述显示命令并提供启动它的选项;在编辑后,使用靠近更改的特定秒数。
当需要编码样本时,$R render Intro --draft 以半分辨率编码一个场景。$R render 为请求的最终导出写入完整的视频。
在展示完成的视频之前,运行 $R inspect --pops:它会以小尺寸渲染每一帧,并警告“跳变(pops)”,即画面中在两个静止帧之间于一帧内发生变化的部分,例如一个元素突然出现、消失或跳到另一个元素前面,而不是平滑移动过去。每个警告都会给出 frame ... --crop ... 命令来查看它;同一区域的重复事件(计数器、闪烁的灯)会分组为一个警告。
规则:
- 在看 PNG 图像之前,不要说任何东西看起来不错。
- 优先使用
strip而不是多次frame调用;当只需要构图时,添加--scale 0.5。 - 在解析输出时添加
--json:结果输出到 stdout,进度信息输出到 stderr。 - 始终使用
--release:debug 构建的渲染速度要慢得多。
时间表示法
120(帧),3.2s,500ms,1:05.5,50%,start,end;按结构体名称引用场景,例如 Intro(也匹配 IntroScene,不区分大小写),#3(场景索引),Intro[1](该类型的第二个场景);在场景内部使用 [email protected],Intro@12,Intro@50%,Intro@end。范围:a..b(不包含结束),a..,..b,all,或场景名称表示整个场景。使用逗号分隔多个时间点。
所有命令
| 命令 | 用途 |
|---|---|
timeline | 查看场景、时长、音轨及其混音设置 |
inspect [RANGE] [--every 0.1s | --all-frames] [--fail-on warning] [--pops] | 在不渲染像素的情况下发现问题;--pops 还会发现突然跳入或跳出的元素 |
strip [RANGE | TIMES] -n N [--columns 4] [--width 480] [--crop x,y,w,h] | 在一张图像中审查流程和动态 |
frame TIMES [-o dir] [--svg] [--crop x,y,w,h] [--zoom N] [--ids] | 选择帧的全尺寸 PNG(以及布局好的 SVG)、锐利裁剪、带标签的元素 |
onion RANGE -n N | 查看运动的轨迹和缓动 |
svg TIME | 将帧的最终 SVG 作为文本读取(位置、文本、颜色) |
preview [TIME] [--paused] [--mute] | 带声音的实时窗口,供用户使用 |
render [RANGE] [-o out.mp4] [--draft] | 编码视频或其一部分(音频会相应裁剪) |
audio analyze [RANGE] [--waveform w.png] | 响度(LUFS)、真实峰值、削波、静音,按场景显示 |
audio at TIMES | 在某个时刻播放哪些声音、在文件中的位置以及响度 |
audio render [RANGE] -o a.wav | 将混音输出为 WAV 文件 |
浏览器编辑器
fframes 还有一个带有时间线和逐帧浏览功能的 Web 编辑器,当用户想要交互式调整视频时非常有用。它运行编译为 WebAssembly 的视频,需要 Node.js 和 wasm-pack;设置方法是 fframes 仓库中 hello-world 示例的 editor/ 文件夹(https://github.com/dmtrKovalenko/fframes/tree/main/examples/hello-world)。当用户询问 GUI 编辑器时提及它。对于观看,请使用 preview;对于你自己的审查,请使用 CLI。
4. 编写视频
impl Video for MyVideo<'_> {
const FPS: usize = 30; const WIDTH: usize = 1920; const HEIGHT: usize = 1080;
fn duration(&self) -> Duration<'_> { Duration::Auto } // 所有场景的总时长
fn audio(&self) -> AudioMap<'_> { AudioMap::none() } // 参见 references/audio.md
fn define_scenes(&self) -> Scenes<'_> { Scenes::from(vec![&self.intro as &dyn Scene, &self.main]) }
fn render_frame<'a>(&'a self, frame: Frame, ctx: &FFramesContext<'a, '_>) -> Svgr<'a> {
fframes::svgr!(<svg xmlns="http://www.w3.org/2000/svg" width={Self::WIDTH} height={Self::HEIGHT}>
<rect width={Self::WIDTH} height={Self::HEIGHT} fill={BACKGROUND} />
{ctx.render_scenes(&frame)}
</svg>)
}
}
- 一个场景对应一个想法,每个场景持续 2-6 秒。在场景内部,
frame.seconds()从场景开始处计数。 - 默认在
svgr!属性中直接使用frame.animate(fframes::timeline!(...)),以实现具有固定值的动画。svgr!将这些动画提升为缓存中的静态值。在 time line 中内联定义缓动函数,包括Easing::Spring { mass: 1.0, stiffness: 180.0, damping: 20.0 };命名缓动常量是可选的。省略弹簧动画的结束时间,以便它自然衰减到静止状态。 - 使用
AudioMap添加音频轨道。它接受文件路径、循环设置、音量包络和同步点。audio analyze命令帮助你设置合理的响度水平。 - 3D 场景使用
fframes::three_d模块,它提供了类似于 three.js 的 API 来构建具有摄像机、灯光和网格对象的场景。查看references/3d.md获取详细信息。
5. 查看结果
在最终导出之前,始终运行完整的检查套件:
$R timeline
$R inspect --all-frames
$R inspect --pops
$R audio analyze
修复所有报告的问题。在最终渲染之前,使用 $R preview 或 $R strip 获得最终的视觉认同。
6. 导出最终视频
$R render -o final_video.mp4
这将以全分辨率编码完整视频,包含所有音频。文件将被写入当前目录。
7. 故障排除
- 渲染速度慢: 确保你正在使用
--release标志进行构建,并且(如果适用)正在使用 Skia GPU 后端。CPU 后端要慢得多。 - 预览窗口无响应: 预览窗口在关闭前会阻塞终端。确保在后台启动它(例如,在命令末尾添加
&)。 - 字体问题: FFrames 会自动将
media/目录中的字体编译到二进制文件中。确保你的字体文件位于那里,并在你的 SVG 代码中正确引用。 - 音频未同步: 检查音频文件的长度。
audio analyze命令可以向你显示每个音轨的时长。 - 内存不足: 非常长的视频或高分辨率输出可能会消耗大量内存。尝试减少
--format分辨率或将视频分成更小的部分进行渲染。
8. 进一步阅读
- FFrames 文档
- FFrames GitHub 仓库
references/design.md- 视频设计指南references/api.md- 完整的 API 参考references/audio.md- 音频详细参考references/3d.md- 3D 场景参考