前言
我们正在开发一款运行在浏览器中的 AI 视频编辑器,支持素材导入、时间线剪辑、字幕、AI 配音、音频处理和视频导出。
早期版本主要支持浏览器原生兼容较好的 MP4 和 WebM。但在真实使用中,用户还会上传 MKV、MOV 等文件,其中可能包含 H.264、H.265、AAC、AC3 等不同编码。它们能在桌面播放器里正常打开,却不一定能在浏览器中直接播放。
为了解决这个问题,我们对媒体处理和导出链路进行了一次改造。
一、格式兼容不只是文件扩展名
MP4、MKV、MOV 和 WebM 是容器,H.264、VP9、AAC 和 AC3 才是音视频编码。
例如,同样是 MKV 文件,内部可能是:
MKV
├── H.264 视频
└── AAC 音频
也可能是:
MKV
├── H.265 视频
└── AC3 音频
所以不能只根据 .mkv 或 .mov 判断文件是否可用。导入时必须进一步识别容器、视频编码、音频编码、时长、分辨率和轨道信息。
我们的处理过程是:
用户导入文件
↓
识别文件签名和容器
↓
探测音视频轨道
↓
检测浏览器解码能力
↓
选择成本最低的处理路径
二、为什么不把所有文件都交给 FFmpeg.wasm?
最直接的办法,是把用户上传的所有文件先转成 MP4:
ffmpeg -i input.mkv -c:v libx264 -c:a aac output.mp4
这种方式兼容性强,但在浏览器中也有明显成本:
- WebAssembly 运行时体积较大;
- 首次初始化需要时间;
- 完整转码速度较慢;
- 原文件、中间数据和输出文件会同时占用内存;
- 即使原视频已经是 H.264,也可能被重复编码。
因此,我们没有移除 FFmpeg.wasm,而是把它从默认入口调整为兼容回退方案。
三、混合媒体处理架构
目前编辑器采用分层处理方式:
浏览器原生能力
↓
LibAV.js + WebCodecs
↓
FFmpeg.wasm 兼容回退
浏览器原生路径
对于标准 MP4、WebM 等浏览器能够直接播放的素材,继续使用 <video>、Web Audio API 等原生能力。这条路径初始化快、内存占用低,也最适合普通用户。
LibAV.js + WebCodecs 路径
对于 MKV、MOV 等浏览器不能直接识别的容器,使用 LibAV.js 探测和解封装轨道,再把兼容的视频数据交给 WebCodecs 解码。
例如,一个 MKV 文件中包含 H.264 视频和 AC3 音频时,可以分别处理:
H.264 视频 → 解封装 → WebCodecs 解码
AC3 音频 → LibAV.js 解码 → PCM/WAV
这样就不必为了一个不兼容的音频轨道而重新编码整段视频。
FFmpeg.wasm 回退路径
如果遇到浏览器和 WebCodecs 都无法处理的编码、异常时间戳或特殊像素格式,再使用 FFmpeg.wasm 完成转码。
这种设计的核心是:能原生处理就不转码,能解封装就不重新编码,只有必要时才进入完整转码流程。
四、性能优化
为了避免扩展格式支持拖慢普通素材,我们做了几项调整。
1. 延迟加载
LibAV.js 和 FFmpeg.wasm 不随编辑器首屏一起初始化。只有遇到确实需要兼容处理的文件时才动态加载。
let runtimePromise: Promise<MediaRuntime> | null = null;
export function getMediaRuntime() {
if (!runtimePromise) {
runtimePromise = import("./media-runtime")
.then((module) => module.createRuntime())
.catch((error) => {
runtimePromise = null;
throw error;
});
}
return runtimePromise;
}
2. 使用 Web Worker
媒体探测、解封装和音频解码会消耗较多 CPU。我们把这些任务放进 Worker,避免阻塞时间线拖动、预览和界面响应。
3. 缓存探测结果
同一个素材的容器、编码、时长和轨道信息只探测一次,缩略图、预览和导出尽量复用这些结果。
4. 支持任务取消
用户删除素材或关闭项目后,后台探测和解码任务会立即终止,避免继续占用 CPU 和内存。
五、统一内部媒体模型
支持更多格式之后,不能让时间线一直感知 MP4、MKV 或 MOV 的具体差异。
无论输入是什么格式,最终都会转换成统一描述:
interface TimelineMediaSource {
id: string;
fileName: string;
container: string;
duration: number;
video?: {
codec: string;
width: number;
height: number;
frameRate: number;
};
audio?: {
codec: string;
sampleRate: number;
channels: number;
};
}
格式兼容层负责处理差异,时间线只处理统一的媒体源、时间范围和帧数据。这样后续增加新格式时,不需要重写剪辑、字幕和导出模块。
六、简化导出选项
底层支持的参数越来越多后,我们曾经在导出界面中提供分辨率、帧率、编码器、关键帧间隔、质量等级和多种预设。
但对多数用户来说,参数过多反而增加了使用成本。因此我们把主要格式精简为:
- MP4 · H.264 + AAC:默认选项,兼容性最好;
- MOV · H.264 + AAC:方便导入 Final Cut、Premiere 和 DaVinci;
- WebM · VP9 + Opus:适合网页和较高压缩率;
- WebM · VP8 + Opus:兼容较旧的 WebM 工作流。
高级参数主要保留视频码率和音频码率。底层能力可以复杂,但产品界面应该简单。
七、修复 5 秒视频导出成 17 秒的问题
改造过程中,我们还发现一个典型问题:浏览器预览中的时间线只有 5 秒,导出结果却长达 17 秒。
原因是旧逻辑把“根据脚本文字估算的配音时长”也用于计算导出范围:
const duration = Math.max(
visualDuration,
audioDuration,
estimatedScriptDuration
);
即使真实时间线只有 5 秒,只要脚本估算为 17 秒,导出器就会继续生成空白帧。
修复后,导出时长只来自时间线中真实存在的素材:
const duration = Math.max(
getVisualTimelineEnd(),
getCaptionTimelineEnd(),
getVoiceTimelineEnd(),
getMusicTimelineEnd(),
getStickerTimelineEnd()
);
帧数也由时间线时长和帧率准确计算:
const frameCount = Math.ceil(duration * frameRate);
5 秒、30 fps 的项目应当导出 150 帧,而不是依赖播放状态或脚本预测值。
八、确定性离线渲染
高质量导出采用逐帧离线渲染:
时间线时间戳
↓
渲染当前画面
↓
WebCodecs 视频编码
↓
OfflineAudioContext 混音
↓
封装为 MP4、MOV 或 WebM
每一帧都由准确时间戳驱动,因此页面卡顿、后台标签页限速或预览掉帧不会改变最终文件的帧数和时长。
MediaRecorder 仍然保留,但主要作为浏览器能力不足时的兼容方案。
九、如何验证导出结果?
导出成功不能只看是否生成了 Blob。自动化测试还需要重新读取产物并验证:
- 容器和编码格式;
- 视频与音频轨道;
- 分辨率;
- 总时长;
- 视频帧数;
- 字幕和贴纸是否出现在画面中;
- 音频轨道是否真实存在。
完整测试流程是:
导入素材
↓
加入时间线
↓
执行导出
↓
重新解析导出文件
↓
验证轨道、时长、帧数和画面
这比只判断界面是否显示“导出完成”更可靠。
总结
浏览器视频编辑器兼容 MKV 和 MOV,并不是简单增加两个文件扩展名,而是要解决容器探测、编解码能力判断、解封装、音频处理、时间戳和导出验证等一整套问题。
我们的最终选择是:
优先浏览器原生处理
必要时使用 LibAV.js + WebCodecs
最后由 FFmpeg.wasm 兼容回退
这样既保留了普通 MP4 的快速体验,也让 MKV、MOV 等素材有机会直接进入浏览器编辑流程。
对于用户来说,最终体验仍然很简单:上传素材、完成编辑、选择格式并导出。复杂的格式判断和媒体处理,应该尽可能留在系统内部。