使用MSE与mp4box.js播放多视频轨道遇阻求助
问题
开发Web应用时,使用Media Source Extensions (MSE)和MP4Box.js从单个MP4文件播放两个独立视频轨道,遇到以下问题:
- 视频元素无任何内容显示
- 已成功追加初始化段和媒体段,但
SourceBuffer.buffered.length始终为0 - 控制台无错误日志输出,确认MIME类型匹配视频编码且浏览器支持该类型
已遵循标准流程:为每个视频元素初始化MediaSource和SourceBuffer,通过MP4Box.js处理文件、提取轨道,先追加初始化段再追加媒体段,但仍无法正常播放。
实现代码
<!DOCTYPE html> <html> <head> <title>Video tracks player</title> <script src="https://gpac.github.io/mp4box.js/dist/mp4box.all.js"></script> </head> <body> <video id="video1" controls width="500"></video> <video id="video2" controls width="500"></video> <script> const video1 = document.getElementById('video1'); const video2 = document.getElementById('video2'); const assetURL = 'video.mp4'; // Replace with your video file path function initVideoPlayer(video, trackIndex) { console.log('Initializing video player', { video, trackIndex }); const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); console.log('MediaSource and video source set', { mediaSource, videoSrc: video.src }); mediaSource.addEventListener('sourceopen', () => { console.log('MediaSource opened', { readyState: mediaSource.readyState }); fetch(assetURL) .then(response => { console.log('Video file fetched', { assetURL, status: response.status }); return response.arrayBuffer(); }) .then(arrayBuffer => { console.log('\n\nArrayBuffer obtained from fetch', { byteLength: arrayBuffer.byteLength }); const mp4boxFile = MP4Box.createFile(); console.log('MP4Box file created'); mp4boxFile.onReady = (info) => { console.log('MP4Box file is ready', { info }); const videoTracks = info.tracks.filter(track => track.type === "video"); console.log('Video tracks filtered', { videoTracks }); if (videoTracks[trackIndex]) { const track = videoTracks[trackIndex]; console.log('Processing track', { trackIndex, track }); const mime = `video/mp4; codecs="${track.codec}"`; const sourceBuffer = mediaSource.addSourceBuffer(mime); console.log('SourceBuffer created', { mime, sourceBuffer }); // Set segment options mp4boxFile.setSegmentOptions(track.id, sourceBuffer, { nbSamples: track.nb_samples }); console.log('Segment options set', { trackId: track.id, nbSamples: track.nb_samples }); // Append initialization segments first const initSegs = mp4boxFile.initializeSegmentation(); initSegs.forEach((initSeg) => { if (initSeg.id === track.id) { sourceBuffer.appendBuffer(initSeg.buffer); console.log('Appended initialization segment', { initSeg }, initSeg.user.buffered); } }); sourceBuffer.addEventListener('updateend', () => { console.log('updateend event after appending init segment', { sourceBuffer }); }); sourceBuffer.addEventListener('error', () => { console.error('Error while appending init segment', { sourceBuffer }); }); // Handle media segments mp4boxFile.onSegment = (id, user, buffer) => { console.log('Received segment', { trackId: id, bufferByteLength: buffer.byteLength }); if (!sourceBuffer.updating) { sourceBuffer.appendBuffer(buffer); console.log('Appended media segment to SourceBuffer', { buffer }); } }; mp4boxFile.start(); console.log('MP4Box file start called'); } }; arrayBuffer.fileStart = 0; mp4boxFile.appendBuffer(arrayBuffer); console.log('Appended ArrayBuffer to MP4Box file', { fileStart: 0 }); }) .catch(error => { console.error('Error while fetching video:', error); }); }); } initVideoPlayer(video1, 0); // Initialize first track initVideoPlayer(video2, 1); // Initialize second track </script> </body> </html>
控制台日志
test.html:22 Initializing video player {video: video#video1, trackIndex: 0} test.html:26 MediaSource and video source set {mediaSource: MediaSource, videoSrc: 'blob:http://127.0.0.1:5500/8b52067c-260e-4ec5-bf56-a25e06493e6d'} test.html:22 Initializing video player {video: video#video2, trackIndex: 1} test.html:26 MediaSource and video source set {mediaSource: MediaSource, videoSrc: 'blob:http://127.0.0.1:5500/8da1916c-ade5-4c8d-a97f-5de14c21e36f'} test.html:29 MediaSource opened {readyState: 'open'} test.html:29 MediaSource opened {readyState: 'open'} test.html:33 Video file fetched {assetURL: 'video.mp4', status: 200} test.html:33 Video file fetched {assetURL: 'video.mp4', status: 200} test.html:31 Fetch finished loading: GET "http://127.0.0.1:5500/video.mp4". (anonymous) @ test.html:31 test.html:31 Fetch finished loading: GET "http://127.0.0.1:5500/video.mp4". (anonymous) @ test.html:31 test.html:37 ArrayBuffer obtained from fetch {byteLength: 8542253} test.html:40 MP4Box file created test.html:43 MP4Box file is ready {info: {…}} test.html:46 Video tracks filtered {videoTracks: Array(2)} test.html:50 Processing track {trackIndex: 0, track: {…}} test.html:54 SourceBuffer created {mime: 'video/mp4; codecs="avc1.4d4028"', sourceBuffer: SourceBuffer}mime: "video/mp4; codecs=\"avc1.4d4028\""sourceBuffer: SourceBufferappendWindowEnd: InfinityappendWindowStart: 0buffered: TimeRanges {length: 0}mode: "segments"onabort: nullonerror: nullonupdate: nullonupdateend: nullonupdatestart: nulltimestampOffset: 0updating: false[[Prototype]]: SourceBuffer[[Prototype]]: Object test.html:58 Segment options set {trackId: 1, nbSamples: 298} test.html:65 Appended initialization segment {initSeg: {…}} TimeRanges {length: 0} test.html:78 Received segment {trackId: 1, bufferByteLength: 5241615} test.html:86 MP4Box file start called test.html:92 Appended ArrayBuffer to MP4Box file {fileStart: 0} test.html:37 ArrayBuffer obtained from fetch {byteLength: 8542253} test.html:40 MP4Box file created test.html:43 MP4Box file is ready {info: {…}} test.html:46 Video tracks filtered {videoTracks: Array(2)} test.html:50 Processing track {trackIndex: 1, track: {…}} test.html:54 SourceBuffer created {mime: 'video/mp4; codecs="avc1.4d4028"', sourceBuffer: SourceBuffer} test.html:58 Segment options set {trackId: 2, nbSamples: 290} test.html:65 Appended initialization segment {initSeg: {…}} TimeRanges {length: 0} test.html:78 Received segment {trackId: 2, bufferByteLength: 3355562} test.html:86 MP4Box file start called test.html:92 Appended ArrayBuffer to MP4Box file {fileStart: 0} test.html:70 updateend event after appending init segment {sourceBuffer: SourceBuffer} test.html:70 updateend event after appending init segment {sourceBuffer: SourceBuffer}
解决方案
问题出在文件加载逻辑、分段追加时机和轨道关联这几个关键点上,以下是修复方案:
关键问题分析
- 重复加载文件:两个播放器独立fetch整个MP4文件,不仅浪费资源,还会导致MP4Box.js处理时的状态冲突
- 分段追加时机错误:未等待初始化段的
updateend事件完成就开始追加媒体段,MSE无法正确解析初始化信息 - 轨道关联不严谨:
onSegment事件未严格验证轨道ID与SourceBuffer的对应关系,可能导致分段追加错误
修改后的完整代码
<!DOCTYPE html> <html> <head> <title>Video tracks player</title> <script src="https://gpac.github.io/mp4box.js/dist/mp4box.all.js"></script> </head> <body> <video id="video1" controls width="500"></video> <video id="video2" controls width="500"></video> <script> const video1 = document.getElementById('video1'); const video2 = document.getElementById('video2'); const assetURL = 'video.mp4'; // 替换为你的视频文件路径 // 仅加载一次视频文件,共享给两个播放器 fetch(assetURL) .then(response => response.arrayBuffer()) .then(arrayBuffer => { const mp4boxFile = MP4Box.createFile(); mp4boxFile.onReady = (info) => { const videoTracks = info.tracks.filter(track => track.type === "video"); if (videoTracks.length >= 2) { initVideoPlayer(video1, videoTracks[0], mp4boxFile); initVideoPlayer(video2, videoTracks[1], mp4boxFile); } }; arrayBuffer.fileStart = 0; mp4boxFile.appendBuffer(arrayBuffer); mp4boxFile.start(); // 统一启动分段处理 }) .catch(error => { console.error('加载视频文件失败:', error); }); function initVideoPlayer(video, track, mp4boxFile) { const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener('sourceopen', () => { const mime = `video/mp4; codecs="${track.codec}"`; const sourceBuffer = mediaSource.addSourceBuffer(mime); // 为当前轨道设置分段选项,关联到对应的SourceBuffer mp4boxFile.setSegmentOptions(track.id, sourceBuffer, { nbSamples: track.nb_samples }); // 先追加初始化段 const initSegs = mp4boxFile.initializeSegmentation(); initSegs.forEach((initSeg) => { if (initSeg.id === track.id) { sourceBuffer.appendBuffer(initSeg.buffer); } }); // 等待初始化段追加完成后,再处理媒体段 sourceBuffer.addEventListener('updateend', () => { mp4boxFile.onSegment = (id, user, buffer) => { // 确保只处理当前轨道的分段,且SourceBuffer未在更新状态 if (id === track.id && user === sourceBuffer && !sourceBuffer.updating) { sourceBuffer.appendBuffer(buffer); } }; }); sourceBuffer.addEventListener('error', (e) => { console.error('SourceBuffer错误:', e); }); }); } </script> </body> </html>
修复点说明
- 共享文件加载:仅执行一次fetch请求,避免重复加载和MP4Box.js的状态冲突
- 调整追加顺序:等待初始化段的
updateend事件触发后,再注册媒体段处理逻辑,确保MSE完成初始化信息解析 - 严格轨道关联:在
onSegment中验证轨道ID和SourceBuffer的匹配关系,避免分段追加到错误的播放器 - 统一启动分段:在文件加载完成后统一调用
mp4boxFile.start(),避免多次启动导致的异常
内容的提问来源于stack exchange,提问作者louislefebvre
相关产品推荐
相关产品推荐

