使用WebCodecs API从JPG生成MP4失败:文件为空如何解决
问题:使用WebCodecs API生成MP4视频时文件为空无法播放
我正在研究WebCodecs API,尝试将一组JPG图片生成MP4视频。先从简单场景入手,用单张JPG图片生成MP4,在output回调中保存视频块到文件,但打开时提示“你尝试播放的文件是空文件”。
我的代码如下:
const downloadVideo = (data) => { console.log("downlaodVideo data:" + data); const link = document.createElement("a"); const file = new Blob([ Uint8Array.from( data ) ] , {type: "application/octet-stream"}); link.href = URL.createObjectURL(file); link.download = "video.h264"; link.click(); URL.revokeObjectURL(link.href); }; $(document).ready(async function () { const videoEncoder = new VideoEncoder( { async output(chunk, metadata) { console.log("encoder queue:" + videoEncoder.encodeQueueSize); console.log("timestamp:" + chunk.timestamp + " counter:" + counter); console.log("length:" + chunk.byteLength); console.log("video chunk type:" + chunk.type); console.log("duration:" + chunk.duration) console.log(JSON.stringify(metadata)); console.log("decoder config:" + metadata.decoderConfig ); var myMetadata; var videoBlob; if (metadata.decoderConfig) { //# save the decoder's description/config ... //# is SPS and PPS metadata needed by players to display H.264) console.log("decoder config description:" + metadata.decoderConfig.description); myMetadata = new Uint8Array( metadata.decoderconfig.description ); } videoBlob = new ArrayBuffer(chunk.byteLength); chunk.copyTo(videoBlob); //# combine the two arrays (in the shown order of appearance) var outputBytes = [...myMetadata, ...videoBlob]; downloadVideo(outputBytes); }, error(error) { console.log(output_Bytes); }, } ); const encoderConfig = { codec: "avc1.42001E", avc: { format: 'annexb' }, height: 480, width: 640, framerate: 1, latencyMode: "realtime", bitrate: 2_000_000, // 2 Mbps }; const support = await VideoEncoder.isConfigSupported(encoderConfig); if (support.supported) { console.log("Video Encoder configured!"); videoEncoder.configure(encoderConfig ); } else { console.error("Video codec not supported!"); } $('#create_movie').click( function(ev) { const myImage = new Image(); myImage.src = "samples/pic0.jpg"; var imageBitmap = null; var imageBitmapPromise = null; myImage.onload = () => { imgContext.drawImage(myImage,0,0); imageBitmapPromise = createImageBitmap(myImage); imageBitmap = imageBitmapPromise.then( result => { const ms = 1_000_000; // 1µs const fps = 10; return new VideoFrame(result, { timestamp: (ms * 1) / fps, duration: ms / fps },false); }).then(vidFrame => { console.log("imageBitmap:" + vidFrame); console.log("video frame timestamp:" + vidFrame.timestamp); console.log("format:" + vidFrame.format); videoEncoder.encode(vidFrame, { keyFrame: true }); vidFrame.close(); }); } }); }
我用FFmpeg工具通过命令ffmpeg -i pic%d.jpg -vcodec mpeg4 test.mp4从同一张JPG生成了正常的MP4,对比两者文件结构发现,我生成的文件缺少FFmpeg生成文件中的部分元数据。请问如何正确使用WebCodecs API生成视频文件?
解决方案
你的代码存在几个关键问题,导致生成的文件无法被播放器识别,以下是修正要点和完整代码:
1. 核心问题分析
- WebCodecs的
VideoEncoder仅输出H.264编码的裸帧数据,不是完整的MP4容器。播放器需要容器格式(如MP4)才能识别,裸流无法直接播放。 - 代码中存在拼写错误:
metadata.decoderconfig应该是metadata.decoderConfig(大小写敏感),导致无法正确获取SPS/PPS元数据。 - 未调用
videoEncoder.flush(),编码器可能还有缓存数据未输出,导致文件不完整。 - 在
output回调中立即下载单块数据,无法拼接完整的流数据。
2. 修正步骤
步骤1:收集所有编码数据
不要在output回调中立即下载,而是先把SPS/PPS元数据和所有帧数据收集到数组中,等待编码完成后统一处理。
步骤2:调用flush()确保所有数据输出
在编码完最后一帧后,必须调用videoEncoder.flush(),触发编码器输出剩余缓存数据。
步骤3:将裸H.264流封装为MP4
浏览器原生没有MP4封装API,有两种可行方案:
- 方案A:收集裸H.264数据后,用本地FFmpeg转封装为MP4(适合测试场景)。
- 方案B:使用FFmpeg.wasm在浏览器内完成MP4封装(无需本地工具)。
以下先提供方案A的修正代码,确保生成有效裸H.264流,再用FFmpeg转成MP4。
3. 修正后的完整代码
// 收集所有编码数据的数组 let encodedData = []; const downloadVideo = () => { // 将收集到的所有数据合并为Blob const blob = new Blob(encodedData, { type: 'video/h264' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'video_raw.h264'; link.click(); URL.revokeObjectURL(link.href); // 提示转封装命令 console.log('请用FFmpeg转封装为MP4: ffmpeg -i video_raw.h264 -c:v copy output.mp4'); }; $(document).ready(async function () { const videoEncoder = new VideoEncoder({ async output(chunk, metadata) { console.log("encoder queue:" + videoEncoder.encodeQueueSize); console.log("timestamp:" + chunk.timestamp); console.log("length:" + chunk.byteLength); console.log("video chunk type:" + chunk.type); console.log("duration:" + chunk.duration); console.log(JSON.stringify(metadata)); let chunkData = new Uint8Array(chunk.byteLength); chunk.copyTo(chunkData); // 处理SPS/PPS元数据(修正拼写错误) if (metadata.decoderConfig?.description) { const spsPps = new Uint8Array(metadata.decoderConfig.description); encodedData.push(spsPps); } // 添加帧数据 encodedData.push(chunkData); }, error(error) { console.error('编码器错误:', error); }, }); const encoderConfig = { codec: "avc1.42001E", avc: { format: 'annexb' }, height: 480, width: 640, framerate: 1, latencyMode: "realtime", bitrate: 2_000_000, // 2 Mbps }; const support = await VideoEncoder.isConfigSupported(encoderConfig); if (support.supported) { console.log("Video Encoder configured!"); videoEncoder.configure(encoderConfig); } else { console.error("Video codec not supported!"); } $('#create_movie').click(async function (ev) { // 重置收集数组 encodedData = []; const myImage = new Image(); // 处理跨域(如果图片来自不同域名) myImage.crossOrigin = 'anonymous'; myImage.src = "samples/pic0.jpg"; myImage.onload = async () => { const imageBitmap = await createImageBitmap(myImage); const ms = 1_000_000; // 微秒单位 const fps = 1; // 单帧视频帧率设为1即可 const vidFrame = new VideoFrame(imageBitmap, { timestamp: 0, // 第一帧时间戳设为0 duration: ms / fps }); console.log("video frame timestamp:" + vidFrame.timestamp); console.log("format:" + vidFrame.format); // 编码关键帧 videoEncoder.encode(vidFrame, { keyFrame: true }); vidFrame.close(); imageBitmap.close(); // 触发编码器输出所有剩余数据 await videoEncoder.flush(); // 所有数据收集完成后下载 downloadVideo(); }; }); });
4. 转封装为MP4
下载得到video_raw.h264后,执行FFmpeg命令完成封装:
ffmpeg -i video_raw.h264 -c:v copy output.mp4
这条命令会将裸H.264流封装到MP4容器中,添加播放器所需的元数据,生成可正常播放的MP4文件。
5. 进阶:浏览器内直接封装MP4
如果需要在浏览器内直接生成MP4,可以使用FFmpeg.wasm库,它允许在浏览器中运行FFmpeg。核心逻辑是将收集到的H.264数据传入FFmpeg.wasm,调用转封装命令生成MP4后再下载。
内容的提问来源于stack exchange,提问作者grandzello
相关产品推荐
相关产品推荐

