Web MediaRecorder API拼接视频块末尾出现空白视频问题求助
我用下面这段代码拼接浏览器录制的第一块和最后一块视频,视频块每3秒收集一次。最终下载的视频确实会按顺序播放首尾两块,但总时长是所有收集过的视频块的总时长,不是首尾两块的时长之和,末尾会有一大段空白。
代码如下(注意原代码有个语法错误,创建Blob时缺了闭合括号,已修正):
let mediaRecorder = new MediaRecorder() let chunks = [] mediaRecorder.start(3000) // 每3秒触发ondataavailable事件 mediaRecorder.ondataavailable = (ev) => { chunks.push(ev.data); if (chunks.length > 2) chunks.splice(1,1) // 只保留第一块和最后一块 } // 原代码此处缺了闭合括号,已补上 let blob = new Blob(chunks, {'type': 'video/mp4;'}) let videoURL = window.URL.createObjectURL(blob) let downloadLink = document.getElementById('downloadLink'); downloadLink.href = videoURL; mediaRecorder.stop(); downloadLink.click()
我猜问题可能是MediaRecorder用ev.timeStamp来算总时长,不是实际保留的视频块时长总和?按道理最后一块应该在总时长的最后3秒播放、中间是空白,但实际播放顺序是「开头块、结尾块、空白部分」。我试过用blob.slice(0, -10000)截Blob末尾,但没用,空白还是去不掉。有没有办法解决这个末尾空白的问题?
怎么解决
核心问题是:MediaRecorder生成的每个视频块都带时间戳元数据,直接拼接首尾块时,播放器会读取所有块的时间戳信息,把总时长算成「第一个块的起始时间」到「最后一个块的结束时间」,中间被你删掉的那些块对应的时间区间就变成空白了。
直接拼Blob肯定不行,得重新处理视频的时间线,让首尾块的时间戳连续接上,这里给两个可行方案:
方案1:用ffmpeg.wasm在浏览器里重新封装(简单易上手)
ffmpeg能自动处理视频的时间戳问题,把首尾两块拼成连续的视频,步骤如下:
- 先引入ffmpeg.wasm库(可通过npm或CDN引入)
- 用下面的代码处理:
// 假设已经引入了@ffmpeg/ffmpeg库 import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'; async function fixVideoBlank(chunks) { const ffmpeg = createFFmpeg({ log: false }); await ffmpeg.load(); // 把两个视频块写入ffmpeg的虚拟文件系统 ffmpeg.FS('writeFile', 'first_chunk.mp4', await fetchFile(chunks[0])); ffmpeg.FS('writeFile', 'last_chunk.mp4', await fetchFile(chunks[1])); // 用ffmpeg的concat滤镜拼接视频,自动修正时间线 await ffmpeg.run( '-i', 'concat:first_chunk.mp4|last_chunk.mp4', '-c', 'copy', // 直接复制编码,不重新编码,速度快 'fixed_video.mp4' ); // 读取处理后的视频文件,转成Blob const fixedData = ffmpeg.FS('readFile', 'fixed_video.mp4'); const fixedBlob = new Blob([fixedData.buffer], { type: 'video/mp4' }); // 创建下载链接 const videoURL = window.URL.createObjectURL(fixedBlob); const downloadLink = document.getElementById('downloadLink'); downloadLink.href = videoURL; downloadLink.click(); // 清理虚拟文件 ffmpeg.FS('unlink', 'first_chunk.mp4'); ffmpeg.FS('unlink', 'last_chunk.mp4'); ffmpeg.FS('unlink', 'fixed_video.mp4'); } // 在mediaRecorder停止后调用 mediaRecorder.onstop = () => { fixVideoBlank(chunks); };
方案2:用WebCodecs API手动调整时间戳(无需第三方库,但复杂度高)
如果不想引入第三方库,可以用浏览器原生的WebCodecs API解析视频帧,调整时间戳后重新编码:
async function mergeChunksWithWebCodecs(chunks) { return new Promise(async (resolve) => { // 先获取第一个块的时长 const firstVideo = document.createElement('video'); firstVideo.src = URL.createObjectURL(chunks[0]); await firstVideo.play(); await firstVideo.pause(); const firstDuration = firstVideo.duration; URL.revokeObjectURL(firstVideo.src); // 解析第二个块的视频帧 const reader = new FileReader(); reader.onload = async () => { const arrayBuffer = reader.result; // 这里需要一个MP4解复用器,可自行实现或用轻量第三方库 // 假设我们有MP4Demuxer工具类可以提取视频帧 const demuxer = new MP4Demuxer(arrayBuffer); const videoFrames = await demuxer.extractVideoFrames(); const audioFrames = await demuxer.extractAudioFrames(); // 调整所有帧的时间戳,让起始时间等于第一个块的时长(WebCodecs用微秒) const timeOffset = firstDuration * 1_000_000; videoFrames.forEach(frame => frame.timestamp += timeOffset); audioFrames?.forEach(frame => frame.timestamp += timeOffset); // 重新编码成MP4 const videoChunks = []; const videoEncoder = new VideoEncoder({ output: (chunk) => videoChunks.push(chunk), error: (err) => console.error('编码错误:', err) }); // 配置编码器参数,和原视频一致 videoEncoder.configure({ codec: 'avc1.42E01E', width: firstVideo.videoWidth, height: firstVideo.videoHeight, bitrate: 5_000_000 }); // 编码所有视频帧 for (const frame of videoFrames) { videoEncoder.encode(frame); frame.close(); } await videoEncoder.flush(); // 同理处理音频,然后合并视频和音频块(此处省略音频处理完整逻辑) // 最后把编码后的块拼成Blob,生成下载链接 resolve(videoChunks); }; reader.readAsArrayBuffer(chunks[1]); }); }
这个方案需要自己处理MP4的解复用和重新封装,复杂度较高,适合对性能有要求的场景,一般推荐用ffmpeg.wasm的方案。
为什么直接切Blob没用?因为MP4的时长信息存在文件的元数据(moov原子)里,不是存在数据末尾,你截掉Blob末尾的字节根本碰不到元数据,所以空白还是会存在。只有重新生成包含正确时间线的元数据,才能解决这个问题。
内容的提问来源于stack exchange,提问作者ultraGentle

