You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用FFmpeg.wasm替代mp4box.js实现视频解复用及编码块、元数据提取

使用FFmpeg.wasm实现视频解复用的方案

核心思路

FFmpeg.wasm是CLI工具的Web移植版,要提取编码块和元数据,需先将视频的音轨、视频轨分别导出为原始码流文件(视频用.h264,音频用.aac),再读取这些文件内容转换成EncodedVideoChunk/EncodedAudioChunk;元数据通过FFmpeg的ffprobe命令直接提取。

具体步骤

1. 加载文件到FFmpeg文件系统

保留你现有逻辑,优化文件读取方式:

public async loadFile(file: File) {
    const data = await file.arrayBuffer();
    await this.ffmpeg.setProgress(({ ratio }) => console.log(`处理进度: ${Math.round(ratio * 100)}%`));
    this.ffmpeg.FS('writeFile', 'videoTest.mp4', new Uint8Array(data));
    // 后续操作写在此处
}

2. 提取元数据

用ffprobe命令输出JSON格式的元数据,直接解析提取所需字段:

// 执行ffprobe获取元数据
const probeOutput = await this.ffmpeg.run(
    '-v', 'quiet',
    '-print_format', 'json',
    '-show_format',
    '-show_streams',
    'videoTest.mp4'
);
// 解析输出的JSON字符串
const metadata = JSON.parse(probeOutput);
// 提取视频元数据
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
const videoMeta = {
    duration: parseFloat(metadata.format.duration),
    timeBase: videoStream.time_base,
    fps: eval(videoStream.r_frame_rate),
    width: videoStream.width,
    height: videoStream.height,
    codec: videoStream.codec_name,
};
// 提取音频元数据
const audioStream = metadata.streams.find(s => s.codec_type === 'audio');
const audioMeta = {
    channels: audioStream.channels,
    sampleRate: audioStream.sample_rate,
    codec: audioStream.codec_name,
};

3. 导出音视频原始码流

用-c:v copy和-c:a copy参数直接复制编码流,避免重新编码:

// 导出视频轨为.h264文件
await this.ffmpeg.run(
    '-i', 'videoTest.mp4',
    '-c:v', 'copy',
    '-an', // 忽略音频
    'videoTrack.h264'
);
// 导出音频轨为.aac文件
await this.ffmpeg.run(
    '-i', 'videoTest.mp4',
    '-c:a', 'copy',
    '-vn', // 忽略视频
    'audioTrack.aac'
);

4. 读取码流并转换为EncodedChunk对象

按编码格式的单元分割码流,生成对应的Chunk对象:

// 处理视频码流,分割H.264的NALU单元
const videoBuffer = this.ffmpeg.FS('readFile', 'videoTrack.h264');
const videoChunks: EncodedVideoChunk[] = [];
let offset = 0;
while (offset < videoBuffer.length) {
    // 查找NALU起始码
    let startCodeLength = 0;
    if (videoBuffer[offset] === 0 && videoBuffer[offset+1] === 0 && videoBuffer[offset+2] === 1) {
        startCodeLength = 3;
    } else if (videoBuffer[offset] === 0 && videoBuffer[offset+1] === 0 && videoBuffer[offset+2] === 0 && videoBuffer[offset+3] === 1) {
        startCodeLength = 4;
    } else break;

    offset += startCodeLength;
    // 查找下一个起始码位置,确定当前NALU长度
    let nextStart = offset;
    while (nextStart < videoBuffer.length - 3) {
        if ((videoBuffer[nextStart] === 0 && videoBuffer[nextStart+1] === 0 && videoBuffer[nextStart+2] === 1) ||
            (videoBuffer[nextStart] === 0 && videoBuffer[nextStart+1] === 0 && videoBuffer[nextStart+2] === 0 && videoBuffer[nextStart+3] === 1)) {
            break;
        }
        nextStart++;
    }
    const naluData = videoBuffer.slice(offset, nextStart);
    // 创建EncodedVideoChunk,时间戳需结合元数据精确计算
    videoChunks.push(new EncodedVideoChunk({
        type: (naluData[0] & 0x1F) === 5 ? 'key' : 'delta',
        data: naluData,
        timestamp: 0, // 需根据帧时间信息设置
        duration: 0 // 需根据帧率计算单帧时长
    }));
    offset = nextStart;
}

// 处理音频码流,分割AAC的ADTS帧
const audioBuffer = this.ffmpeg.FS('readFile', 'audioTrack.aac');
const audioChunks: EncodedAudioChunk[] = [];
offset = 0;
while (offset < audioBuffer.length) {
    // 解析ADTS头获取帧长度
    if (audioBuffer[offset] === 0xFF && (audioBuffer[offset+1] & 0xF0) === 0xF0) {
        const frameLength = ((audioBuffer[offset+3] & 0x03) << 11) | (audioBuffer[offset+4] << 3) | ((audioBuffer[offset+5] & 0xE0) >> 5);
        const adtsFrame = audioBuffer.slice(offset, offset + frameLength);
        audioChunks.push(new EncodedAudioChunk({
            type: 'delta',
            data: adtsFrame,
            timestamp: 0, // 需根据采样率计算时间戳
            duration: 0 // 需根据帧内采样数计算时长
        }));
        offset += frameLength;
    } else break;
}

5. 清理临时文件

处理完成后删除FFmpeg文件系统中的临时文件:

this.ffmpeg.FS('unlink', 'videoTest.mp4');
this.ffmpeg.FS('unlink', 'videoTrack.h264');
this.ffmpeg.FS('unlink', 'audioTrack.aac');

关键注意点

  • 选择.h264和.aac是因为它们是无容器的原始编码流,便于分割成单个编码块。
  • 时间戳和时长需结合元数据中的时间基准、帧率、采样率精确计算,否则会出现音画不同步。
  • FFmpeg.wasm的run命令是异步的,需按顺序执行或用Promise.all处理并行任务。

内容的提问来源于stack exchange,提问作者Kevin Baving

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 11:22:47