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>
目前视频缓冲和当前位置显示功能完全无法正常工作,完整代码可查看在线沙箱,请求排查问题。
问题排查与修复方案
核心问题点
useEffect依赖缺失,duration未正确获取
视频的duration是异步加载的,初始渲染时大概率为NaN。空依赖的useEffect仅执行一次,此时无法正确计算inc(画布宽度与视频时长的比例),导致后续绘制坐标全部错误。事件监听覆盖不全
仅监听seeked事件无法覆盖所有场景:
- 缓冲进度更新需要监听
progress事件(视频缓冲数据增加时触发) - 当前播放位置更新需要监听
timeupdate事件(播放进度变化时触发)
Canvas尺寸未正确初始化
仅通过CSS设置Canvas宽高,未设置元素本身的width和height属性,会导致绘制坐标被拉伸,出现显示错位。画布未清空重绘
每次绘制缓冲区域前未重新填充背景,多次绘制会导致内容重叠,无法展示最新状态。状态管理冗余
用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
相关产品推荐
相关产品推荐

