OpenMontage Remotion 最佳实践:用 Mediabunny 精确读取音频时长(浏览器 / Node.js / Bun)
本文是 OpenMontage 仓库内 Agent 技能集 remotion-best-practices 中 get-audio-duration 规则(原文档,并在 .claude 技能集中有一份同步副本)的深度展开。它讲解如何借助 Mediabunny 在任意运行环境里把一段 MP3/WAV 音频的时长精确地读取为秒数,并在 Remotion 合成中用 staticFile() 指向本地资源;文章同时结合本仓库 remotion-composer 的真实秒→帧换算与音频编排实践,说明拿到时长后如何驱动合成时长、音画对齐与自动校验。读完你将得到一份可直接复制进项目的音频测时工具函数,以及一套与 OpenMontage Remotion 渲染链路打通的使用方案。
Mediabunny 是什么,为什么用它测音频时长
Mediabunny 是一个跨运行时(browser、Node.js、Bun 均可用)的媒体探测与解析库,能够从音频文件容器中提取时长(duration)等元信息。它不需要依赖 FFmpeg 子进程即可工作,因此在 Remotion 的浏览器渲染环境、服务端渲染环境里表现一致。
在 OpenMontage 的 Remotion 工程中,Mediabunny 并不是需要单独手写的引入来源:remotion-composer/package.json 将 @remotion/media(^4.0.484)列为直接依赖,而在 remotion-composer/package-lock.json 中可以看到 mediabunny(解析版本 1.47.0)以及 @mediabunny/aac-encoder、@mediabunny/mp3-encoder、@mediabunny/flac-encoder 等编解码扩展被一并解析安装。也就是说,只要你的 Remotion 项目按 audio 规则 执行过:
npx remotion add @remotion/media
Mediabunny 的 Input、UrlSource、FileSource 等 API 就已就绪,可直接 import 使用,无需额外声明新依赖。
最小可用示例:从 URL 读取音频时长
规则文档给出的核心函数是 getAudioDuration:构造一个 Input,声明接受所有媒体容器格式(ALL_FORMATS),数据源使用 UrlSource,然后调用 computeDuration() 获得以秒为单位的浮点时长。
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getAudioDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};
对这份代码的逐项说明:
formats: ALL_FORMATS:告知Input在探测时覆盖 Mediabunny 支持的全部容器/编码格式。规则文档的标签(mp3、wav)表明常见的压缩与无损音频都在覆盖范围内;当你后续处理视频(同源get-video-duration规则)时,同样的配置也适用于含音轨的容器。new UrlSource(src, { getRetryDelay: () => null }):UrlSource表示源是网络地址。回调getRetryDelay用于控制请求失败后的重试策略——这里始终返回null,即不安排任何自动重试,让失败快速向上抛给调用方处理。computeDuration():阻塞式完成容器解析并返回总时长(秒)。
调用与返回值
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
console.log(duration); // e.g. 180.5 (seconds)
返回值是秒为单位的浮点数(示例中 180.5 表示 3 分 0.5 秒),这一点对后续秒→帧换算至关重要——不要把它当作毫秒或帧数。
在 Remotion 中指向 public/ 目录的资源:staticFile()
Remotion 约定将静态资源放在 public/ 目录,并在组件内通过 staticFile() 取得其编译期可知的 URL。测时长时若直接拼接 "audio.mp3" 这类相对路径,在 Studio 预览、打包渲染等不同环境下都可能解析失败,因此规则文档明确要求用 staticFile() 包一层:
import { staticFile } from "remotion";
const duration = await getAudioDuration(staticFile("audio.mp3"));
这与 Remotion 中加载音频资源的方式完全一致。以本仓库的音频层实现为例,OpenMontage 的渲染链路会把 narration、music 等素材按并行 <Audio> 轨组织(参见 skills/core/remotion.md 的 “Audio Layering” 一节),所有素材都通过 staticFile() 或已解析的绝对 URL 进入合成器;因此在测量“public 内本地音频”与“远端音频”时,唯一的差别就是这一层包装。
Node.js 与 Bun:改用 FileSource
浏览器环境无法随意读取用户本地磁盘路径,必须走 UrlSource(或用于上传场景的 BlobSource);而在 Node.js / Bun 这类具备完整文件系统能力的运行时里,规则文档推荐直接使用 FileSource:
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const durationInSeconds = await input.computeDuration();
注意 FileSource 接收的是 File 对象(来自文件上传 <input>、拖拽等),并非裸路径字符串。若你希望在 Node 脚本中按磁盘路径读取,需要先经由对应的文件读取 API 产出 File/Blob 形态的对象再传入——这也保证了同一套 Input.computeDuration() 逻辑在三个运行时里行为一致。
三种数据源的选择时机
| 数据源 | 运行环境 | 适用场景 |
|---|---|---|
UrlSource |
浏览器 / Node / Bun | 远端 URL(CDN 音频、TTS 返回的临时链接) |
FileSource |
Node / Bun / 浏览器 | 已上传或拖拽得到的 File 对象 |
BlobSource |
浏览器为主 | Blob 形态的临时音频数据(参考 can-decode 规则中的 BlobSource 用法) |
同一 Input 只更换 source 实现,其余探测逻辑(formats、computeDuration())完全复用,这正是把测时逻辑封装为独立工具函数、在合成前统一调度的价值所在。
拿到秒数之后:秒 → 帧换算与合成时长驱动
音频时长本身的最终用途通常是决定/校验 Remotion 合成的总时长。Remotion 的时间单位是帧,因此需要做 seconds × fps 换算。规则文档中的姊妹篇 calculate-metadata 给出了与 getVideoDuration 配合的经典写法(音频同理):
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
const durationInSeconds = await getAudioDuration(props.audioSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
};
};
这里用 Math.ceil 向上取整,保证合成帧数至少覆盖音频全长,避免片尾被截断。与之对应,OpenMontage 的 remotion-composer 工程中到处可见这套“秒×fps→帧”的约定:CinematicRenderer.tsx 里场景排布使用 Math.round(scene.startSeconds * FPS) 与 Math.round(scene.durationSeconds * FPS) 作为 <Sequence> 的 from 与 durationInFrames,并在 calculateCinematicMetadata 中以 Math.max(1, Math.ceil(totalSeconds * FPS)) 反推整片时长(见 CinematicRenderer.tsx)。可见“外部拿秒、内部换帧”是本仓库所有合成器的统一数据契约,getAudioDuration 的返回值可直接喂给这类元数据函数。
与 getVideoDuration 的区分
规则集中同时存在 get-video-duration 规则:两者的 API 形态完全一致,仅语义不同——视频时长含画面轨道与音轨的交叠总长,音频时长则是单条音轨的总长。当素材是“视频容器但只需取其配音时长”或“纯音频决定画面”时,按内容类型分别选用即可。
在 OpenMontage 渲染链路中的真实落地:音画对齐与自动校验
Mediabunny 的测时函数适合在合成器代码中动态计算;而在 OpenMontage 更高层的编排侧,音频时长还承担着“渲染前门禁校验”的职责。根据 skills/core/remotion.md 的说明:
- TTS 旁白生成后,工具会返回
audio_duration_seconds;若旁白超过视频总时长,需要缩短脚本重新生成,或延长最后一个场景,否则会出现音画截断; - 背景音乐时长应 ≥ 视频时长(由 Remotion 的
fadeOutSeconds负责收尾淡出),不足时开启loop; - 任何音频文件都可用
tools.analysis.audio_probe.probe_duration(path)(Python 侧实现)核对时长; - 渲染前的
composition_validator会检查“旁白音轨长于视频”“音乐短于视频”等问题,result.data["valid"]必须为True才允许进入渲染。
这给出了一条完整的技术分工:Python/编排侧用 ffprobe 类工具(audio_probe)做预渲染校验,Remotion/渲染侧用 Mediabunny 做动态元数据计算——两者互补,Mediabunny 方案的价值在于它不依赖外部二进制,在浏览器 Studio 预览与无头渲染两种场景下给出相同结果。当你需要把“探测、解码可用性、时长、分辨率”串成一套前端工具链时,可顺带参考同目录下的 can-decode、get-video-duration 与 get-video-dimensions 规则,它们的 Input 构造骨架与本例高度一致。
使用注意事项小结
- 运行时差异只体现在 Source 类型上:浏览器/URL 用
UrlSource,Node/Bun 本地文件用FileSource,上传场景用BlobSource;Input、formats、computeDuration()三处保持不变。 - 返回值是秒(浮点):进入
durationInFrames前必须乘以fps,并建议Math.ceil向上取整,避免音轨尾段超出合成范围。 - 不要用裸相对路径测
public/内资源:务必staticFile("audio.mp3")后再传入,保证 Studio 与渲染环境路径一致。 - 重试策略可关可开:
getRetryDelay: () => null表示失败即抛;生产环境中若音频源位于不稳定网络,可改为返回递增延迟以换取鲁棒性,但需自行处理超时。 - 本规则属于技能路由体系:
remotion-best-practices技能在 SKILL.md 中将本规则与 audio、get-video-duration、calculate-metadata 等并列编目。当 Agent 面对“音频时长/长度/时间(秒)”类诉求时,get-audio-duration(tags:duration、audio、length、time、seconds、mp3、wav)即是最佳匹配入口——这一定位正是本文件以规则卡(YAML frontmatter)形式存在于 Agent 技能仓库中的原因。
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 StartedRust0631
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证件照制作算法。Python09
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