You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 06:00:54