OpenMontage Remotion 资源引用指南:public 目录、staticFile() 与远程素材的正确接入方式
本文系统讲解 OpenMontage 项目中 Remotion 组合(composition)如何引用图片、视频、音频与字体等资产:静态资源统一放入项目根目录的
public/文件夹,通过staticFile()引用以获得可正确部署到子目录的编码 URL;并配合仓库源码与测试,说明一套兼容"远程 URL / 绝对文件路径 / public 相对路径"的资源解析层如何在真实代码中落地。
规则文档的来源与角色
本篇文章围绕的技能规则文件位于 .agents/skills/remotion-best-practices/rules/assets.md,它是 OpenMontage 的 remotion-best-practices 技能包(SKILL.md)下的一条独立规则,同时在 .claude/skills/remotion-best-practices/rules/assets.md 存有镜像副本,供不同编码助手(Agent)按各自约定的技能目录加载。
OpenMontage 是一套"把 AI 编码助手变成视频制作工作室"的开源项目,而 Remotion 承担了其中组合式渲染的职责。仓库中的 remotion-composer 目录就是一个真实可运行的 Remotion 工程,负责将节目按场景类型渲染成最终视频。因此,"资产如何进入 Remotion、如何被引用"是每一个需要写 Remotion 代码的 Agent 都必须遵守的基础契约——这正是本规则存在的意义:凡是处理 Remotion 代码,都应加载本技能获取领域知识,而 assets 规则给出了资产引用部分的统一标准。
资产引用的三条原则速览
在深入细节前,先把规则的核心结论提炼如下,后文逐一展开:
- 静态资产统一放在项目根目录的
public/文件夹,任何 import 语句不应直接去node_modules或任意磁盘路径取资源; - 引用
public/内的文件必须使用staticFile()包裹,它返回一个编码后的 URL,能在部署到子目录时依然正确解析; - 远程 URL(http/https)可以直接传给组件,无需
staticFile()处理。
public 文件夹:静态资产的唯一归宿
Remotion 约定,任何希望在运行时被组合引用的静态文件——Logo、照片、剪辑好的视频片段、背景音乐、字体文件——都应放置在项目根目录下的 public/ 文件夹中。
在 OpenMontage 的 remotion-composer 工程里,这个目录即 remotion-composer/public。例如当前仓库中该目录下存放了 demo-props/ 等演示数据;而实际流水线(pipeline)产出的媒体,则会被前序阶段**组装(staging)**进该目录,供 Remotion 渲染时通过固定前缀路径读取。这一点在测试注释中得到了印证:tests/tools/test_remotion_media_staging.py 的开头说明中写道,若某一步骤假设"assets 已在 remotion-composer/public/ 中"而实际未被前序阶段安置,将导致运行时 404。
为什么强调"项目根目录的 public/",而不是随便某个子目录?因为 Remotion 打包时以工程根目录为准来计算静态资源根路径;
staticFile()的参数一律是从public/内部开始计量的相对路径。
用 staticFile() 引用 public 内的文件
规则文档强调了一条强制要求:必须使用 staticFile() 引用 public/ 文件夹中的文件,而不是手写相对路径或直接拼接字符串。
import { Img, staticFile } from "remotion";
export const MyComposition = () => {
return <Img src={staticFile("logo.png")} />;
};
staticFile() 的核心价值在于:它返回一个经过正确编码的 URL,即使最终站点被部署到某个子路径下(而非域名根路径),资源也能被正确加载——这与 Web 开发中"不要写死绝对根路径"的原则一脉相承。
在 OpenMontage 的 remotion-composer 源码中,可以看到该函数被大量真实使用。例如 fixtures.ts 在定义组合演示数据时,为背景音乐、视频片段统一包裹 staticFile():
import { staticFile } from "remotion";
export const signalFromTomorrowWithMusicFixture = {
soundtrack: {
src: staticFile("music/signal-from-tomorrow/cinematic_time_hans_zimmer_style.mp3"),
volume: 0.42,
fadeInSeconds: 1.5,
fadeOutSeconds: 2.5,
},
scenes: [
{
id: "sc1",
kind: "video",
startSeconds: 0,
durationSeconds: 4,
src: staticFile("video/signal-from-tomorrow/sample_observatory_veo31_ref.mp4"),
},
// ... 更多场景
],
};
可见约定路径形如 music/...mp3、video/...mp4,均相对 remotion-composer/public/ 解析。
四类核心资产的组件接入模板
规则文档按媒体类型给出了可直接复制的模板,本文完整保留并稍作注释说明。
图片(Img)
import { Img, staticFile } from "remotion";
export const MyComposition = () => {
return <Img src={staticFile("photo.png")} />;
};
图片是 Remotion 组合中最常见的静态资产。OpenMontage 中许多场景组件都用到了此模式,例如 ProductReveal.tsx 在展示产品图时,将 productImage 以 staticFile(productImage) 形式传入 <Img>。针对图片还有专门的规则文档 images.md 可进一步参考(GIF 则有 gifs.md)。
视频(Video)
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Video src={staticFile("clip.mp4")} />;
};
说明:原规则示例从
@remotion/media导入Video组件。在 OpenMontage 实际工程中,视频轨通常用OffthreadVideo(从remotion导入)承担渲染,例如 TitledVideo.tsx 中src传入统一解析函数的结果。视频相关更完整的玩法(裁剪、音量、倍速、循环、音调)参见 videos.md。
音频(Audio)
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("music.mp3")} />;
};
音乐与音效是 Remotion 组合的常客。需要更丰富处理(时长获取、裁剪、音量控制)时可查阅 audio.md;@remotion/media-utils 的 getVideoMetadata 等工具还能在渲染前异步读取媒体元数据——TitledVideo 的元数据计算就依赖它,见下文源码分析。
字体(FontFace)
import { staticFile } from "remotion";
const fontFamily = new FontFace("MyFont", `url(${staticFile("font.woff2")})`);
await fontFamily.load();
document.fonts.add(fontFamily);
字体的特殊之处在于它不是组件,而是需要先注册到浏览器的 FontFace 体系:用 staticFile() 拼出字体文件 URL,构造 FontFace,等待 load() 完成后通过 document.fonts.add() 注入,之后才能在样式中以 fontFamily 名称使用。OpenMontage 仓库 assets 目录(如 assets/patrickhand.ttf)存在真实字体文件,且 ink-theater 子项目使用同类 TTF 作为手写风字幕字体。关于字体的更多细节参见专门规则 fonts.md。
远程 URL:免 staticFile 直接使用
当素材托管在远程服务器上时,src 可以直接使用完整 URL,不需要(也不应该)用 staticFile() 包裹:
<Img src="https://example.com/image.png" />
<Video src="https://remotion.media/video.mp4" />
远程 URL 透传逻辑在 OpenMontage 的源码中也有对应实现:下面的解析函数会先判断 URL 是否以 http://、https:// 或 data: 开头,是则原样返回,不再做任何本地路径处理。
两个必须牢记的细节
规则文档以"Important notes"收尾,提出两条对渲染正确性至关重要的注意事项:
- Remotion 组件(
<Img>、<Video>、<Audio>)会确保资产在渲染前被完全加载。这意味着你不要在组件外手动做加载竞态处理——组件体系已内置"先加载完再进入渲染帧"的语义,这也是为什么必须使用 Remotion 提供的组件而非裸<img>/<video>标签。 - 文件名中的特殊字符(如
#、?、&)会被自动编码。因此即便资源文件名含有上述字符,也只需在staticFile()中按原始文件名书写即可,URL 编码由框架自动完成,无需手工encodeURIComponent。
仓库源码印证:从规则到统一解析层
规则解决"怎么写得对",而 OpenMontage 工程中还沉淀了一层统一资源解析(resolveAsset),把本规则进一步扩展为对"public 相对路径 / 远程 URL / 磁盘绝对路径"三种输入的兼容处理。这段实现位于 remotion-composer/src/lib/resolveAsset.ts:
import { staticFile } from "remotion";
const isRemoteAsset = (src: string): boolean =>
src.startsWith("http://") ||
src.startsWith("https://") ||
src.startsWith("data:");
const isWindowsAbsolutePath = (src: string): boolean =>
/^[A-Za-z]:[\\/]/.test(src);
/** Resolve public assets and absolute filesystem paths consistently. */
export function resolveAsset(src: string): string {
if (isRemoteAsset(src)) {
return src; // 1) 远程 URL 直接透传
}
const withoutScheme = src.replace(/^file:\/\//i, "");
const clean = /^\/[A-Za-z]:[\\/]/.test(withoutScheme)
? withoutScheme.slice(1)
: withoutScheme;
if (clean.startsWith("/") || isWindowsAbsolutePath(clean)) {
const posix = clean.replace(/\\/g, "/");
return posix.startsWith("/") ? `file://${posix}` : `file:///${posix}`;
} // 2) 绝对路径 -> file:// 协议
return staticFile(clean); // 3) 其余一律走 public/ + staticFile()
}
逐行对照本规则:
- 远程 URL:
resolveAsset对http://、https://、data:前缀做白名单判定并原样返回——正是"远程 URL 可直接使用"这条规则的工程化实现; - public 相对路径:不以
/开头、也不是 Windows 盘符绝对路径的输入,最终落入staticFile(clean)——即"public 内的文件必须用 staticFile() 引用"; - 绝对文件路径:额外把本地磁盘路径转换为
file://协议,这是对规则在"流水线产物与前端 public 资产混合场景"下的必要扩展,仍以兼容静态文件系统为底线。
resolveAsset 的实际消费点在 TitledVideo.tsx:
<OffthreadVideo
src={resolveAsset(videoSrc)}
// ...
/>
同时其元数据计算函数 calculateTitledVideoMetadata 也通过 getVideoMetadata(resolveAsset(props.videoSrc)) 在渲染前获取视频时长,从而动态计算组合的帧数与 fps:
export const calculateTitledVideoMetadata: CalculateMetadataFunction<
TitledVideoProps
> = async ({ props }) => {
const meta = await getVideoMetadata(resolveAsset(props.videoSrc));
// ... 用 meta 计算 durationInFrames / fps 并返回
};
这展示了一个完整链路:资产引用 → 统一解析 → Remotion 组件消费 → 元数据驱动组合时长,与本规则文档的静态约定一一对应。
测试如何守护这条契约
仓库的契约测试同样约束了"资产必须可被 public 路径寻址"这一前提。在 tests/tools/test_remotion_media_staging.py 中,测试断言了诸如 anime_scene.images[]、screenshot_scene.backgroundImage 这类字段会被当作 staticFile() 语义下的 public 资产来对待;文件头注释还点出:这些资产若依赖"前序流水线阶段已把媒体写入 remotion-composer/public/"而实际缺失,则渲染时必然 404。也就是说,资产放置(public staging)与资产引用(staticFile)是同一契约的两端,写 Remotion 代码的 Agent 需要同时对两端负责。
Agent 使用指引:何时读取这条规则
作为技能规则,assets.md 的触发时机非常明确:
- 需要为 Remotion 组合引入任何图片、视频、音频或字体资产时,先加载本规则;
- 当处理字幕、FFmpeg 操作、音频可视化等衍生话题时,加载 SKILL.md 中列出的对应规则文件(如 subtitles.md、ffmpeg.md);
- 与资产紧密相关的细化规则还包括 images.md、videos.md、audio.md、fonts.md 与 display-captions.md,可按需级联加载。
编码助手(Claude Code、Cursor、Codex 等)会依据各自约定的技能目录(.agents/ 或 .claude/)加载同一份规则内容,从而保证不同 Agent 在生成 Remotion 代码时,对资产引用的写法保持一致:文件进 public/,引用走 staticFile(),远程素材直连 URL。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00