sourceBuffer.appendBuffer成功但H5视频播放器卡顿,无法续播求助
问题:H5视频分段播放卡顿,进度条显示剩余片段未加载完成
实现H5视频分段播放时遇到问题:sourceBuffer.appendBuffer执行成功,但H5视频播放器卡顿,进度条显示剩余视频片段未加载完成,无法继续播放,且控制台无任何错误信息。
前端React.js代码
import React, { useEffect, useRef } from 'react'; const VideoPage = () => { const videoInited = useRef(false) useEffect(() => { const video = document.querySelector("#stream-media-video") as HTMLMediaElement; // 使用Bento4解析mp4视频文件 // mp4info test.mp4 | grep Codec const mimeCodec = 'video/mp4; codecs="avc1.64081F, mp4a.40.2"'; if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) { const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener("sourceopen", sourceOpen); } else { console.error("不支持的MIME类型或编码: ", mimeCodec); } function sourceOpen(_) { console.log('[mediaSource sourceopen事件触发]: ', this.readyState); // open const mediaSource = this; const playBtn = document.querySelector('.play-btn'); const init = () => { const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); // 通过xhr请求获取视频片段(总文件1.5M,先取前1M,再取后0.5M) fetchAB("/api/media/testmp4/part-v2?start=0&end=1000000", function (buf) { sourceBuffer.addEventListener('error', (e) => { console.error("sourceBuffer错误", e); }); sourceBuffer.addEventListener("updateend", function (_) { // mediaSource.endOfStream(); video.play(); videoInited.current = true; playBtn.removeEventListener("click", init); }); sourceBuffer.appendBuffer(buf); // 获取下一个0.5M片段 fetchAB("/api/media/testmp4/part-v2?start=1000000&end=2000000", function (secondBuf){ sourceBuffer.appendBuffer(secondBuf); }); }); } playBtn.addEventListener("click", init) } function fetchAB(url, cb) { const xhr = new XMLHttpRequest(); xhr.open("get", url); xhr.responseType = "arraybuffer"; xhr.onload = function () { cb(xhr.response); }; xhr.send(); } }, []) const handleClickPlay = () => { const video = document.querySelector("video") if (videoInited.current) { video.play() } } return ( <div> <video id='stream-media-video' width="400px" height="400px" style={{ objectFit: 'contain', }} controls /> <div> <button className='play-btn' onClick={handleClickPlay}>点击播放视频</button> </div> </div> ) } export default VideoPage
后端Express.js代码
app.get("/api/media/testmp4/part-v2", function (req, res) { const { start, end, } = req.query; const mp4FilePath = "test.mp4"; const _start = Number(start); const videoSize = fs.statSync(mp4FilePath).size; const _end = Math.min(Number(end), videoSize - 1); // 获取视频片段并返回给前端 const videoStream = fs.createReadStream(mp4FilePath, { start: _start, end: _end, }); videoStream.pipe(res); });
问题截图

解决方案
1. 确保分段数据对齐MP4关键帧和容器结构
MP4不能按任意字节切割,直接按固定区间拆分会破坏文件元数据(如moov原子)或关键帧,导致MediaSource无法解析后续片段。
- 后端不要直接按字节切割,需先解析MP4的关键帧位置,确保每个分段起始点为关键帧;
- 或用ffmpeg将原视频转成符合MediaSource标准的fMP4分片:
ffmpeg -i test.mp4 -c:v libx264 -c:a aac -f dash manifest.mpd
2. 串行执行appendBuffer操作
当前代码并行调用两次appendBuffer,但sourceBuffer处于updating状态时,后续调用会静默失败。必须等待前一个分片的updateend事件触发后,再添加下一个分片。
修改前端init函数:
const init = () => { const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); sourceBuffer.addEventListener('error', (e) => { console.error("sourceBuffer错误", e); }); // 按顺序加载分片 const loadNextSegment = (segments, index) => { if (index >= segments.length) { // 所有分片加载完成,标记流结束 mediaSource.endOfStream(); video.play(); videoInited.current = true; playBtn.removeEventListener("click", init); return; } const [url] = segments[index]; fetchAB(url, (buf) => { const onUpdateEnd = () => { sourceBuffer.removeEventListener("updateend", onUpdateEnd); loadNextSegment(segments, index + 1); }; sourceBuffer.addEventListener("updateend", onUpdateEnd); sourceBuffer.appendBuffer(buf); }); }; // 定义分片列表 const segments = [ ["/api/media/testmp4/part-v2?start=0&end=1000000"], ["/api/media/testmp4/part-v2?start=1000000&end=2000000"] ]; loadNextSegment(segments, 0); };
3. 后端添加正确的响应头
返回分片时需设置Content-Type、Content-Range等头,让浏览器正确识别分片数据:
app.get("/api/media/testmp4/part-v2", function (req, res) { const { start, end } = req.query; const mp4FilePath = "test.mp4"; const _start = Number(start); const videoSize = fs.statSync(mp4FilePath).size; const _end = Math.min(Number(end), videoSize - 1); const chunkSize = _end - _start + 1; // 设置响应头 res.set({ "Content-Type": "video/mp4", "Content-Length": chunkSize, "Content-Range": `bytes ${_start}-${_end}/${videoSize}`, "Accept-Ranges": "bytes" }); const videoStream = fs.createReadStream(mp4FilePath, { start: _start, end: _end }); videoStream.pipe(res); });
4. 所有分片加载完成后调用endOfStream
当所有分片添加完毕,必须调用mediaSource.endOfStream()告知MediaSource流已结束,否则播放器会认为还有数据未加载,导致进度条卡住。
内容的提问来源于stack exchange,提问作者West
相关产品推荐
相关产品推荐

