控制HTML5 video元素绘制到canvas时出现延迟的原因及解决办法
问题根因分析
1. Load按钮无法一次性加载完成
- video标签未配置预加载策略,默认不会提前加载完整视频资源,点击Load时设置currentTime会触发异步加载流程,无法立即完成
- Load逻辑未等待视频元数据加载完成就直接读取duration、绘制首帧,此时duration可能为NaN,导致进度条设置和首帧绘制失效
- 未做双视频加载状态同步判断,单视频加载完成不代表整个加载流程结束
2. 倍速调整后播放结束异常+卡顿
- 倍速调整逻辑仅修改了全局
playbackRate变量,未同步赋值给两个video元素的playbackRate属性,且播放按钮硬编码播放速度为0.2,调整的倍速值实际未生效 drawVideoFrame递归调用requestAnimationFrame时未做销毁判断,视频暂停/结束后仍持续触发无用重绘,造成内存泄漏和卡顿,同时视频结束重置到首帧时残留的重绘逻辑会引发画面跳动- 仅监听了左侧视频的
ended事件,右侧视频结束无处理逻辑,双视频播放进度不同步时会出现画面不一致的异常 - 逐帧切换的步进值固定为1/60,和视频实际帧率不匹配时会出现跳帧、进度不准的问题
最优解决方案
基础配置修改
给video标签添加预加载和播放相关属性:
<video id="left_video" src="res/left.mp4" preload="auto" muted playsinline></video> <video id="right_video" src="res/right.mp4" preload="auto" muted playsinline></video>
核心逻辑修改
新增raf销毁逻辑,避免无用重绘
// 新增两个变量存储raf请求ID let leftRafId = null; let rightRafId = null; const drawVideoFrame = (direction) => { switch(direction) { case 'left': leftCanvasContext.drawImage(leftVideo, 0, 0, leftCanvas.width, leftCanvas.height); leftRafId = requestAnimationFrame(() => { drawVideoFrame('left'); }); break; case 'right': rightCanvasContext.drawImage(rightVideo, 0, 0, rightCanvas.width, rightCanvas.height); rightRafId = requestAnimationFrame(() => { drawVideoFrame('right'); }); break; } }; // 暂停时取消raf mediaPauseButton.addEventListener('click', () => { leftVideo.pause(); rightVideo.pause(); leftRafId && cancelAnimationFrame(leftRafId); rightRafId && cancelAnimationFrame(rightRafId); });
修复倍速逻辑
mediaPlaybackRateButton.forEach((element) => { element.addEventListener('click', (event) => { playbackRate = parseFloat(event.target.innerText); // 同步修改视频播放速度 leftVideo.playbackRate = playbackRate; rightVideo.playbackRate = playbackRate; }); }); // 播放按钮使用全局倍速变量,不要硬编码 mediaPlayButton.addEventListener('click', () => { leftVideo.playbackRate = playbackRate; rightVideo.playbackRate = playbackRate; leftVideo.play(); rightVideo.play(); console.info(`PLAYBACK RATE VALUE : ${parseFloat(playbackRate).toFixed(1)}`); });
修复Load逻辑,等待视频元数据加载完成
mediaLoadButton.addEventListener('click', async () => { // 等待两个视频元数据加载完成 const waitMetaLoad = (video) => new Promise(resolve => { if(video.readyState >= 2) return resolve(); video.addEventListener('loadedmetadata', resolve, {once: true}); }); await Promise.all([waitMetaLoad(leftVideo), waitMetaLoad(rightVideo)]); loadVideoFirstFrame('left'); loadVideoFirstFrame('right'); mediaSeekBar.min = 0; mediaSeekBar.max = leftVideo.duration; leftCanvasContext.drawImage(leftVideo, 0, 0, leftCanvas.width, leftCanvas.height); rightCanvasContext.drawImage(rightVideo, 0, 0, rightCanvas.width, rightCanvas.height); });
修复视频结束逻辑,同步双视频状态
// 记录双视频结束状态 let leftEnded = false; let rightEnded = false; const handleVideoEnd = () => { if(leftEnded && rightEnded) { // 两个视频都结束才执行重置逻辑 leftRafId && cancelAnimationFrame(leftRafId); rightRafId && cancelAnimationFrame(rightRafId); loadVideoFirstFrame('left'); loadVideoFirstFrame('right'); mediaSeekBar.value = 0; leftCanvasContext.drawImage(leftVideo, 0, 0, leftCanvas.width, leftCanvas.height); rightCanvasContext.drawImage(rightVideo, 0, 0, rightCanvas.width, rightCanvas.height); // 重置状态 leftEnded = false; rightEnded = false; } }; leftVideo.addEventListener('ended', () => { leftEnded = true; handleVideoEnd(); }); rightVideo.addEventListener('ended', () => { rightEnded = true; handleVideoEnd(); });
内容的提问来源于stack exchange,提问作者Minwoo Kim
相关产品推荐
相关产品推荐

