如何用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
相关产品推荐
相关产品推荐

