控制 HTML5 个视频元素时延迟的原因是什么?

What causes delay when controlling HTML5 video elements?

我看了很多相关文章,但一直无法找到明确的原因,说明为什么本地执行的与视频控制相关的功能会导致这种延迟。还有,因为是很久以前写的,所以还是有区别的。

关键是在<canvas>上绘制2个视频图像。在此过程中,我们创建了控制视频播放、暂停、播放速率和帧间移动的函数。

但是,当我按下 Load 按钮时,视频不会立即加载,如果我 运行 视频,请执行 运行 播放速率更改和 运行 又是它,当视频结束时它的行为很奇怪。

使用的两个视频都是 4 秒。代码是否需要优化?还是我写的代码逻辑错了?

我很好奇如何最好地解决它。

// FPS
const FPS = 1 / 60;

const leftVideo  = document.querySelector('#left_video');
const rightVideo = document.querySelector('#right_video');

const leftCanvas  = document.querySelector('#left_canvas');
const rightCanvas = document.querySelector('#right_canvas');

leftCanvas.width = 256;
leftCanvas.height = 256;

rightCanvas.width = 256;
rightCanvas.height = 256;

const leftCanvasContext  = leftCanvas.getContext('2d');
const rightCanvasContext = rightCanvas.getContext('2d');

const mediaLoadButton          = document.querySelector('#media_load');
const mediaPlayButton          = document.querySelector('#media_play');
const mediaPauseButton         = document.querySelector('#media_pause');
const mediaPreviousFrameButton = document.querySelector('#media_previous_frame');
const mediaNextFrameButton     = document.querySelector('#media_next_frame');
const mediaPlaybackRateButton  = document.querySelectorAll('.media_playback_rate');

const mediaSeekBar = document.querySelector('#media_seekbar');

const updateVideoTime = () => {
    mediaSeekBar.value = leftVideo.currentTime;

    mediaSeekBar.style.backgroundSize = (mediaSeekBar.value - mediaSeekBar.min) * 100 / (mediaSeekBar.max - mediaSeekBar.min) + '% 100%';
};

const updateSeekBar = (event) => {
    const location = (event.offsetX / mediaSeekBar.offsetWidth) * leftVideo.duration;

    leftVideo.currentTime  = location;
    rightVideo.currentTime = location;
};

const loadVideoFirstFrame = (direction) => {
    switch(direction) {
        case 'left':
            if(!isNaN(leftVideo.duration)) {
                leftVideo.currentTime = 0;
            }
            break;
        case 'right':
            if(!isNaN(rightVideo.duration)) {
                rightVideo.currentTime = 0;
            }
            break;
    }
};

const drawVideoFrame = (direction) => {
    switch(direction) {
        case 'left':
            leftCanvasContext.drawImage(leftVideo, 0, 0, leftCanvas.width, leftCanvas.height);
            requestAnimationFrame(() => { drawVideoFrame('left'); });
            break;
        case 'right':
            rightCanvasContext.drawImage(rightVideo, 0, 0, rightCanvas.width, rightCanvas.height);
            requestAnimationFrame(() => { drawVideoFrame('right'); });
            break;
    }
};

let playbackRate = 1.0;
let videoMousedown = false;

mediaLoadButton.addEventListener('click', () => {
    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);
});

leftVideo.addEventListener('play', () => {
    drawVideoFrame('left');
});

leftVideo.addEventListener('timeupdate', updateVideoTime, false);

leftVideo.addEventListener('ended', () => {
    loadVideoFirstFrame('left');
    loadVideoFirstFrame('right');

    mediaSeekBar.value = 0;
    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);
});

rightVideo.addEventListener('play', () => {
    drawVideoFrame('right');
});

mediaPlayButton.addEventListener('click', () => {
    leftVideo.playbackRate  = 0.2;
    rightVideo.playbackRate = 0.2;

    leftVideo.play();
    rightVideo.play();

    console.info(`PLAYBACK RATE VALUE : ${parseFloat(playbackRate).toFixed(1)}`);
});

mediaPauseButton.addEventListener('click', () => {
    leftVideo.pause();
    rightVideo.pause();
});

mediaPreviousFrameButton.addEventListener('click', () => {
    leftVideo.currentTime  = Math.max(0, leftVideo.currentTime - FPS);
    rightVideo.currentTime = Math.max(0, rightVideo.currentTime - FPS);
});

mediaNextFrameButton.addEventListener('click', () => {
    leftVideo.currentTime  = Math.min(leftVideo.duration, leftVideo.currentTime + FPS);
    rightVideo.currentTime = Math.min(rightVideo.duration, rightVideo.currentTime + FPS);
});

mediaSeekBar.addEventListener('click', updateSeekBar);
mediaSeekBar.addEventListener('mousemove', (event) => videoMousedown && updateSeekBar(event));
mediaSeekBar.addEventListener('mousedown', () => videoMousedown = true);
mediaSeekBar.addEventListener('mouseup', () => videoMousedown = false);

mediaPlaybackRateButton.forEach((element) => {
    element.addEventListener('click', (event) => {
        playbackRate = event.target.innerText;
    });
});
<div class="container">
    <div class="media-wrapper">
        <!-- left video -->
        <video id="left_video" src="res/left.mp4"></video>
        <!-- right video -->
        <video id="right_video" src="res/right.mp4"></video>
        <!-- left canvas for left video -->
        <canvas id="left_canvas"></canvas>
        <!-- right canvas for right video -->
        <canvas id="right_canvas"></canvas>
    </div>
    <div class="media-controller-wrapper">
        <input id="media_seekbar" type="range" step="any" value="0" min="0" max="100" onchange="updateVideoTime()"/>
        <button id="media_load" type="button">Load</button>
        <button id="media_play" type="button">Play</button>
        <button id="media_pause" type="button">Pause</button>
        <button id="media_previous_frame" type="button">Previous frame</button>
        <button id="media_next_frame" type="button">Next frame</button>
        <button class="media_playback_rate" type="button">1.0</button>
        <button class="media_playback_rate" type="button">0.8</button>
        <button class="media_playback_rate" type="button">0.6</button>
        <button class="media_playback_rate" type="button">0.4</button>
        <button class="media_playback_rate" type="button">0.2</button>
    </div>
</div>

我发现并想分享的一些问题:

我注意到在您的 JS 文件中您重复使用

document.querySelector()(对于
而不是
document.getElementById()(对于 id

例如:

const leftVideo  = document.querySelector('#left_video');
// instead of 
const leftVideo  = document.getElementById('left_video');

我在您的代码中进行了所有适当的修改,现在 代码片段至少可以运行 (将视频的 src 替换为 public 视频 link测试):

编辑,考虑有用的评论:
我在视频标签中添加了以下内容:

    根据我在 Dynamically using the first frame as poster in HTML5 video
  1. 已添加type="video/mp4"
  2. 已添加preload="auto"
    这有助于在 首次单击 的“加载”
  3. 时加载 poster

因此我们现在有:

<video id="left_video" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4#t=0.1" type="video/mp4" preload="auto"></video>

只要按“加载”,post用户应该立即出现:

// FPS
const FPS = 1 / 60;

const leftVideo  = document.getElementById('left_video');
const rightVideo = document.getElementById('right_video');

const leftCanvas  = document.getElementById('left_canvas');
const rightCanvas = document.getElementById('right_canvas');

leftCanvas.width = 256;
leftCanvas.height = 256;

rightCanvas.width = 256;
rightCanvas.height = 256;

const leftCanvasContext  = leftCanvas.getContext('2d');
const rightCanvasContext = rightCanvas.getContext('2d');

const mediaLoadButton          = document.getElementById('media_load');
const mediaPlayButton          = document.getElementById('media_play');
const mediaPauseButton         = document.getElementById('media_pause');
const mediaPreviousFrameButton = document.getElementById('media_previous_frame');
const mediaNextFrameButton     = document.getElementById('media_next_frame');
const mediaPlaybackRateButton  = document.querySelectorAll('.media_playback_rate');

const mediaSeekBar = document.getElementById('media_seekbar');

const updateVideoTime = () => {
    mediaSeekBar.value = leftVideo.currentTime;

    mediaSeekBar.style.backgroundSize = (mediaSeekBar.value - mediaSeekBar.min) * 100 / (mediaSeekBar.max - mediaSeekBar.min) + '% 100%';
};

const updateSeekBar = (event) => {
    const location = (event.offsetX / mediaSeekBar.offsetWidth) * leftVideo.duration;

    leftVideo.currentTime  = location;
    rightVideo.currentTime = location;
};

const loadVideoFirstFrame = (direction) => {
    switch(direction) {
        case 'left':
            if(!isNaN(leftVideo.duration)) {
                leftVideo.currentTime = 0;
            }
            break;
        case 'right':
            if(!isNaN(rightVideo.duration)) {
                rightVideo.currentTime = 0;
            }
            break;
    }
};

const drawVideoFrame = (direction) => {
    switch(direction) {
        case 'left':
            leftCanvasContext.drawImage(leftVideo, 0, 0, leftCanvas.width, leftCanvas.height);
            requestAnimationFrame(() => { drawVideoFrame('left'); });
            break;
        case 'right':
            rightCanvasContext.drawImage(rightVideo, 0, 0, rightCanvas.width, rightCanvas.height);
            requestAnimationFrame(() => { drawVideoFrame('right'); });
            break;
    }
};

let playbackRate = 1.0;
let videoMousedown = false;

mediaLoadButton.addEventListener('click', () => {
    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);
});

leftVideo.addEventListener('play', () => {
    drawVideoFrame('left');
});

leftVideo.addEventListener('timeupdate', updateVideoTime, false);

leftVideo.addEventListener('ended', () => {
    loadVideoFirstFrame('left');
    loadVideoFirstFrame('right');

    mediaSeekBar.value = 0;
    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);
});

rightVideo.addEventListener('play', () => {
    drawVideoFrame('right');
});

mediaPlayButton.addEventListener('click', () => {
    leftVideo.playbackRate  = 0.2;
    rightVideo.playbackRate = 0.2;

    leftVideo.play();
    rightVideo.play();

    console.info(`PLAYBACK RATE VALUE : ${parseFloat(playbackRate).toFixed(1)}`);
});

mediaPauseButton.addEventListener('click', () => {
    leftVideo.pause();
    rightVideo.pause();
});

mediaPreviousFrameButton.addEventListener('click', () => {
    leftVideo.currentTime  = Math.max(0, leftVideo.currentTime - FPS);
    rightVideo.currentTime = Math.max(0, rightVideo.currentTime - FPS);
});

mediaNextFrameButton.addEventListener('click', () => {
    leftVideo.currentTime  = Math.min(leftVideo.duration, leftVideo.currentTime + FPS);
    rightVideo.currentTime = Math.min(rightVideo.duration, rightVideo.currentTime + FPS);
});

mediaSeekBar.addEventListener('click', updateSeekBar);
mediaSeekBar.addEventListener('mousemove', (event) => videoMousedown && updateSeekBar(event));
mediaSeekBar.addEventListener('mousedown', () => videoMousedown = true);
mediaSeekBar.addEventListener('mouseup', () => videoMousedown = false);

mediaPlaybackRateButton.forEach((element) => {
    element.addEventListener('click', (event) => {
        playbackRate = event.target.innerText;
    });
});
@charset "UTF-8";

html, body {
  background-color: #242424;
}

video {
  display: none;
}

canvas {
  width: 256px;
  height: 256px;
}

input[type="range"] {
  -webkit-appearance: none;
  width             : 100%;
  height            : 1px !important;
  background        : #FFFFFF;
  border-radius     : 0px;
  background-image  : linear-gradient(#FF0000, #FF0000);
  background-size   : 0% 100%;
  background-repeat : no-repeat;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width             : 8px;
  height            : 8px;
  border-radius     : 50%;
  background-color  : #FFFFFF;
  cursor            : pointer;
  box-shadow        : 0 0 2px 0 #555555;
  transition        : background .3s ease-in-out;
}

input[type="range"]::-moz-range-thumb {
  -webkit-appearance: none;
  width             : 8px;
  height            : 8px;
  border-radius     : 50%;
  background-color  : #FFFFFF;
  cursor            : pointer;
  box-shadow        : 0 0 2px 0 #555555;
  transition        : background .3s ease-in-out;
}

input[type="range"]::-ms-thumb {
  -webkit-appearance: none;
  width             : 8px;
  height            : 8px;
  border-radius     : 50%;
  background-color  : #FFFFFF;
  cursor            : pointer;
  box-shadow        : 0 0 2px 0 #555555;
  transition        : background .3s ease-in-out;
}

input[type="range"]::-webkit-slider-thumb:hover {
  background        : #FF0000;
}

input[type="range"]::-moz-range-thumb:hover {
  background        : #FF0000;
}

input[type="range"]::-ms-thumb:hover {
  background        : #FF0000;
}

input[type="range"]::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  box-shadow        : none;
  border            : none;
  background        : transparent;
}

input[type="range"]::-moz-range-track {
  -webkit-appearance: none;
  box-shadow        : none;
  border            : none;
  background        : transparent;
}

input[type="range"]::-ms-track {
  -webkit-appearance: none;
  box-shadow        : none;
  border            : none;
  background        : transparent;
}

#media_seekbar {
  width: 512px;
  height: auto;
}
<div class="container">
  <div class="media-wrapper">
    <!-- left video -->
    <video id="left_video" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4#t=0.1" type="video/mp4"></video>
    <!-- right video -->
    <video id="right_video" src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4#t=0.1" type="video/mp4"></video>
    <!-- left canvas for left video -->
    <canvas id="left_canvas"></canvas>
    <!-- right canvas for right video -->
    <canvas id="right_canvas"></canvas>
  </div>
  <div class="media-controller-wrapper">
    <input id="media_seekbar" type="range" step="any" value="0" min="0" max="100" onchange="updateVideoTime()"/>
    <button id="media_load" type="button">Load</button>
    <button id="media_play" type="button">Play</button>
    <button id="media_pause" type="button">Pause</button>
    <button id="media_previous_frame" type="button">Previous frame</button>
    <button id="media_next_frame" type="button">Next frame</button>
    <button class="media_playback_rate" type="button">1.0</button>
    <button class="media_playback_rate" type="button">0.8</button>
    <button class="media_playback_rate" type="button">0.6</button>
    <button class="media_playback_rate" type="button">0.4</button>
    <button class="media_playback_rate" type="button">0.2</button>
  </div>
</div>