控制 HTML5 视频元素时导致延迟的原因是啥?

Posted

技术标签:

【中文标题】控制 HTML5 视频元素时导致延迟的原因是啥?【英文标题】:What causes delay when controlling HTML5 video elements?控制 HTML5 视频元素时导致延迟的原因是什么? 【发布时间】:2021-10-30 07:09:09 【问题描述】:

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

关键是在<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>

【问题讨论】:

第 1 步:记得减少帖子的代码。如果您的问题是关于视频,那么您应该能够在删除与视频无关的一切时重现您的问题(例如,没有CSS,没有媒体控制,没有Canvas元素等)。将你的代码变成minimal reproducible example 并强迫自己删除一些东西,直到你只剩下最少的代码几乎总是让你自己找到问题,因为在某些时候你删除了一些东西,问题突然消失了:你只是找到了什么造成的。但如果没有,你现在有完美的代码来发帖了。 你为什么使用 querySelector 而不是我?例如,QuerySelectoe 非常适合在列表中查找 .selected。 请注意,没有理由坚持使用getElementById 而不是querySelector。这不是性能瓶颈,并且会产生相同的结果(nullhtmlElement)。唯一的区别是您的字符串是否包含#。 querySelector 和 querySelectorAll 函数不是“用于类”,它们是“用于查询选择器”,包括 id、标记名、类、属性以及有效查询选择器可能包含的所有其他内容,并使用它们代替 getElementById、getElementsByTagName、等等都很好。 @Jürgen Fink 我在发帖后看到了你的回答,这就是 +1 的来源……我,干杯。 历史。 getElementById 987654332 querySelector 2087654332 @几乎十年,Ququery甚至没有添加QuerySelector,直到jQuery显示了能够通过CSS查询选择器获得更多有用的有用,多年来。 span> 【参考方案1】:

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

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

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

例如:

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

我在您的代码中进行了所有适当的修改,现在至少 code sn-p 运行(将视频的 src 替换为公共视频链接进行测试):

编辑,考虑有用的评论: 我添加到以下视频标签:

    根据我在Dynamically using the first frame as poster in HTML5 video 的帖子,#t=0.1 添加到视频标签的来源,以加速视频的海报开头 添加了type="video/mp4" 添加了preload="auto" 这有助于在首次点击“加载”时加载海报

因此我们现在有:

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

只需按“加载”,海报应该立即出现

// 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>

【讨论】:

Load 函数是一个海报函数,用于显示视频Play 之前的第一帧。目前至少需要点击 2 次才能加载第一帧,有没有其他方法可以解决这个问题? @MinwooKim 我在答案中编辑了代码,根据我的帖子Dynamically using the first frame as poster in HTML5 video 将#t=0.1 添加到&lt;video&gt; 标签的来源。我对其进行了测试,现在海报出现得更快。尝试让我们知道它是否改善了您的海报加载。 @MinwooKim 还将preload="auto" 添加到&lt;video&gt; 标签。现在,海报在“加载”的第一次点击时加载? 我能够找到并理解您提到的preload#t 选项。感谢分享好知识。

以上是关于控制 HTML5 视频元素时导致延迟的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 HTML5 视频设置音频延迟(不同步)

域名转移失败一般是啥原因?

Android 浏览器在每次 play() 后卸载 HTML5 音频元素,导致延迟

仅在视频播放时进行视频控制 (html5)

RxCocoa - 当有延迟时防止多个视图控制器推送

在页面的其余部分完成加载后延迟加载 html5 视频