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

控制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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 21:54:03