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

React+TypeScript中用Canvas展示视频缓冲与当前时长问题排查

问题:React+TypeScript中Canvas实现视频缓冲功能失效排查

我参考相关文档尝试用Canvas实现视频缓冲状态显示,采用React+TypeScript替代原生JavaScript,用useEffect替代window.onload事件,核心实现代码如下:

现有关键代码

初始化逻辑(useEffect)

useEffect(() => {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    if (canvas && video) {
      var context = canvas.getContext("2d");
      if (context && video.duration) {
        context.fillStyle = "lightgray";
        context.fillRect(0, 0, canvas.width, canvas.height);
        context.fillStyle = "red";
        context.strokeStyle = "white";
        setInc(canvas.width / video.duration);
      }
    }
  }, []);

seeked事件处理函数

const onSeekedEvent = () => {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    if (canvas && video) {
      var context = canvas.getContext("2d");
      if (context) {
        for (var i = 0; i < video.buffered.length; i++) {
          var startX = video.buffered.start(i) * inc;
          var endX = video.buffered.end(i) * inc;
          let width = endX - startX;
          context.fillRect(startX, 0, width, canvas.height);
          context.rect(startX, 0, width, canvas.height);
          context.stroke();
        }
      }
    }
  };

组件结构

<div className="App">
      <video
        width="320"
        height="240"
        muted
        ref={videoRef}
        onSeeked={onSeekedEvent}
      >
        <source
          src="https://www.w3schools.com/html/movie.mp4"
          type="video/mp4"
        />
      </video>
      <div className="canvasDiv" ref={bufferDivRef}>
        <canvas className="bufferCanvas" ref={canvasRef}></canvas>
      </div>
      <button className="btn" onClick={playVideo}>
        Play
      </button>
      <button className="btn" onClick={stopVideo}>
        Pause
      </button>
    </div>

目前视频缓冲和当前位置显示功能完全无法正常工作,完整代码可查看在线沙箱,请求排查问题。


问题排查与修复方案

核心问题点

  1. useEffect依赖缺失,duration未正确获取
    视频的duration是异步加载的,初始渲染时大概率为NaN。空依赖的useEffect仅执行一次,此时无法正确计算inc(画布宽度与视频时长的比例),导致后续绘制坐标全部错误。

  2. 事件监听覆盖不全
    仅监听seeked事件无法覆盖所有场景:

  • 缓冲进度更新需要监听progress事件(视频缓冲数据增加时触发)
  • 当前播放位置更新需要监听timeupdate事件(播放进度变化时触发)
  1. Canvas尺寸未正确初始化
    仅通过CSS设置Canvas宽高,未设置元素本身的width和height属性,会导致绘制坐标被拉伸,出现显示错位。

  2. 画布未清空重绘
    每次绘制缓冲区域前未重新填充背景,多次绘制会导致内容重叠,无法展示最新状态。

  3. 状态管理冗余
    用setInc存储比例值属于冗余操作,直接在绘制时计算即可,避免React状态更新带来的额外渲染问题。

修复后的关键代码

1. 完善初始化与事件监听逻辑

useEffect(() => {
  const video = videoRef.current;
  const canvas = canvasRef.current;
  if (!canvas || !video) return;

  // 初始化Canvas实际尺寸(与视频宽度一致,高度设为20)
  canvas.width = video.offsetWidth;
  canvas.height = 20;
  const context = canvas.getContext("2d");
  if (!context) return;

  // 绘制灰色背景
  const drawBackground = () => {
    context.fillStyle = "lightgray";
    context.fillRect(0, 0, canvas.width, canvas.height);
  };

  // 统一绘制缓冲区域与当前进度的函数
  const drawBufferAndProgress = () => {
    if (!video.duration) return;
    const inc = canvas.width / video.duration;
    drawBackground();

    // 绘制缓冲区域
    context.fillStyle = "red";
    context.strokeStyle = "white";
    for (let i = 0; i < video.buffered.length; i++) {
      const startX = video.buffered.start(i) * inc;
      const endX = video.buffered.end(i) * inc;
      const width = endX - startX;
      context.fillRect(startX, 0, width, canvas.height);
      context.rect(startX, 0, width, canvas.height);
      context.stroke();
    }

    // 绘制当前播放位置标记
    context.fillStyle = "blue";
    const currentX = video.currentTime * inc;
    context.fillRect(currentX, 0, 2, canvas.height);
  };

  // 绑定所需事件
  video.addEventListener("loadedmetadata", drawBufferAndProgress);
  video.addEventListener("progress", drawBufferAndProgress);
  video.addEventListener("timeupdate", drawBufferAndProgress);
  video.addEventListener("seeked", drawBufferAndProgress);

  // 组件卸载时清理事件监听
  return () => {
    video.removeEventListener("loadedmetadata", drawBufferAndProgress);
    video.removeEventListener("progress", drawBufferAndProgress);
    video.removeEventListener("timeupdate", drawBufferAndProgress);
    video.removeEventListener("seeked", drawBufferAndProgress);
  };
}, []);

2. 简化组件结构

移除onSeeked属性和单独的onSeekedEvent函数,同时简化播放/暂停逻辑:

<div className="App">
  <video
    width="320"
    height="240"
    muted
    ref={videoRef}
  >
    <source
      src="https://www.w3schools.com/html/movie.mp4"
      type="video/mp4"
    />
  </video>
  <div className="canvasDiv">
    <canvas className="bufferCanvas" ref={canvasRef}></canvas>
  </div>
  <button className="btn" onClick={() => videoRef.current?.play()}>
    Play
  </button>
  <button className="btn" onClick={() => videoRef.current?.pause()}>
    Pause
  </button>
</div>

修复说明

  • 统一绘制逻辑到单个函数,避免重复代码与状态不同步问题
  • 监听loadedmetadata事件确保视频元数据加载完成后再执行绘制
  • 通过progress实时更新缓冲区域,timeupdate实时更新播放位置
  • 初始化Canvas元素的真实尺寸,保证绘制坐标准确
  • 每次绘制前清空画布,避免内容重叠

内容的提问来源于stack exchange,提问作者rabner casandra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 10:18:33