MediaSource API:如何让Safari避免缓冲区下溢时暂停视频
让Safari在MediaSource缓冲区下溢时保持Chrome式的播放行为
我正在用MediaSource API实现低延迟视频流:通过WebRTC数据通道接收H.264直播流,在浏览器端封装成碎片化MP4后传入MediaSource。
场景中经常出现数据供应不足的情况,原因包括:
- 画面无变化时服务器不发送帧
- 网络波动导致数据延迟
遇到缓冲区下溢时,Chrome和Safari的行为差异明显:
- Chrome会等待新数据到达,之后继续播放,保持
HTMLVideoElement.currentTime与SourceBuffer末尾间的稳定短缓冲 - Safari会立即暂停视频,必须调用
HTMLVideoElement.play()强制恢复,同时要把currentTime更新到接近SourceBuffer末尾的位置,这会导致明显卡顿
有没有办法让Safari实现Chrome的播放逻辑?推测Chrome的行为和其MediaSource实现中的「低延迟模式」有关(由视频流信息触发),是否能在Safari中触发类似模式?
可行解决方案
1. 模拟Chrome的缓冲等待逻辑
监听视频的pause事件,区分用户手动暂停和缓冲区空导致的暂停,仅在后者发生时等待新数据,而非立即强制播放:
let isWaitingForBuffer = false; videoElement.addEventListener('pause', () => { if (mediaSource.readyState === 'open' && sourceBuffer.buffered.length > 0) { const bufferEnd = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1); // 判定是否为缓冲耗尽导致的暂停 if (videoElement.currentTime >= bufferEnd - 0.1 && fragmentQueue.length === 0) { isWaitingForBuffer = true; } } }); // 新数据追加后自动恢复播放 function feedSourceBufferFromQueue() { if (fragmentQueue.length === 0) { if (isWaitingForBuffer && videoElement.paused) { videoElement.play().catch(e => console.error(e)); isWaitingForBuffer = false; } return; } // 原有数据追加逻辑 const nextData = fragmentQueue[0]; fragmentQueue = fragmentQueue.slice(1); sourceBuffer.appendBuffer(nextData); }
2. 显式设置SourceBuffer为序列模式
确保SourceBuffer使用sequence模式,避免播放器自动调整缓冲策略:
sourceBuffer = mediaSource.addSourceBuffer(CODEC_MIME_TYPE); sourceBuffer.mode = 'sequence';
3. 优化碎片化MP4封装
确保流格式符合低延迟要求,帮助Safari识别直播场景:
- 每个MOOF/MDAF单元时长控制在100ms以内
- 关键帧前携带完整的SPS/PPS信息(或在初始化段中正确注入)
- 避免冗余元数据
4. 利用Safari专属API监控缓冲状态
通过webkitVideoDecodedByteCount精准判断新数据到达时机,自动恢复播放:
let lastDecodedBytes = 0; if ('webkitVideoDecodedByteCount' in videoElement) { setInterval(() => { const decodedBytes = videoElement.webkitVideoDecodedByteCount; if (decodedBytes > lastDecodedBytes && isWaitingForBuffer) { videoElement.play(); isWaitingForBuffer = false; } lastDecodedBytes = decodedBytes; }, 50); }
复现代码(集成优化逻辑)
<!DOCTYPE html> <html> <body> <p id="text">Click anywhere to start streaming video</p> <video autoplay playsinline id="video" width="800" height="450"></video> <script> /** @type HTMLVideoElement */ const videoElement = document.getElementById('video'); /** @type HTMLParagraphElement */ const textElement = document.getElementById('text'); async function start() { let fileReadPointer = 0; let fragmentQueue = []; let isWaitingForBuffer = false; let lastDecodedBytes = 0; const file = await fetch('video.dat'); const fileBytes = await file.arrayBuffer(); const CODEC_MIME_TYPE = 'video/mp4; codecs="avc1.424028"'; const mediaSource = new MediaSource(); mediaSource.addEventListener('sourceopen', handleMediaSourceOpen); const mediaSourceUrl = URL.createObjectURL(mediaSource); videoElement.src = mediaSourceUrl; let sourceBuffer; function receiveNextFragment() { if (fileReadPointer + 4 > fileBytes.byteLength) { return; } const length = (new DataView(fileBytes)).getUint32(fileReadPointer); fileReadPointer += 4; const data = new Uint8Array(fileBytes, fileReadPointer, length); fileReadPointer += length; fragmentQueue.push(data); if (!sourceBuffer.updating) { feedSourceBufferFromQueue(); } } function feedSourceBufferFromQueue() { if (fragmentQueue.length === 0) { if (isWaitingForBuffer && videoElement.paused) { videoElement.play().catch(e => console.error(e)); isWaitingForBuffer = false; } return; } const nextData = fragmentQueue[0]; fragmentQueue = fragmentQueue.slice(1); sourceBuffer.appendBuffer(nextData); } function handleMediaSourceOpen() { sourceBuffer = mediaSource.addSourceBuffer(CODEC_MIME_TYPE); sourceBuffer.mode = 'sequence'; sourceBuffer.addEventListener('updateend', ev => { feedSourceBufferFromQueue(); }); videoElement.addEventListener('pause', () => { if (mediaSource.readyState === 'open' && sourceBuffer.buffered.length > 0) { const bufferEnd = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1); if (videoElement.currentTime >= bufferEnd - 0.1 && fragmentQueue.length === 0) { isWaitingForBuffer = true; } } }); if ('webkitVideoDecodedByteCount' in videoElement) { setInterval(() => { const decodedBytes = videoElement.webkitVideoDecodedByteCount; if (decodedBytes > lastDecodedBytes && isWaitingForBuffer) { videoElement.play(); isWaitingForBuffer = false; } lastDecodedBytes = decodedBytes; }, 50); } setInterval(() => { receiveNextFragment(); textElement.innerText = `Video paused: ${videoElement.paused}`; }, 100); } }; let started = false; function handleClick() { if (started) return; started = true; start(); } document.addEventListener('click', handleClick); </script> <style> video { border: 1px solid black; } </style> </body> </html>
内容的提问来源于stack exchange,提问作者Will
相关产品推荐
相关产品推荐

