1

I've looked at a number of related articles, but I haven't been able to find a clear reason why video control-related functions that are made to be performed locally are causing such lag. Also, there are differences because it was written so long ago.

The key is to draw 2 video images on the <canvas>. In the course of this, we created functions to control the video play, pause, playback rate, and moving between frames.

However, when I press the Load button the video doesn't load all at once, and if I run the video, do a run playback rate change and run it again, it behaves strangely when the video is finished.

Both videos used are 4 seconds long. Is the code in need of optimization? Or is the logic of the code I wrote wrong?

I'm curious how best to solve it.

// 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>
Jürgen Fink
  • 3,162
  • 2
  • 23
  • 25
Minwoo Kim
  • 497
  • 1
  • 5
  • 21
  • 2
    step 1: remember to reduce your code for a post. If your question is about video, then you should be able to reproduce your problem while removing everything that isn't related to the video (e.g. no CSS, no media controls, no canvas elements, etc. etc.). Turning your code into a [mcve] and forcing yourself to remove things until you're left with the bare minimum code almost always lets you already find the problem yourself because at some point you remove something and suddenly the problem disappears: you just find what caused it. But if not, you now have _perfect_ code for putting in a post. – Mike 'Pomax' Kamermans Aug 31 '21 at 16:57
  • @Mike'Pomax'Kamermans in general, I **agree 100%** with you. In this particular case, however, all code presented was the minimum required in order to _"understand and use to reproduce the problem"_ as stated in [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) and based on that I was able to find the errors and make the code snippet work (see in answer below). But again, in general you are absolutely right and contributors often don't follow the mentioned guidelines and post unnecessary code in their posts. – Jürgen Fink Aug 31 '21 at 18:39
  • 1
    Why are you using querySelector instead of I'd? QuerySelectoe is good for looking for a .selected over a list, as an example. – norcal johnny Aug 31 '21 at 19:00
  • @norcaljohnny **You are right**, however, this has been mentioned (**and fixed** already) in answer from 1 hour ago (see further below). – Jürgen Fink Aug 31 '21 at 19:10
  • 1
    Note that there is no reason to insist on `getElementById` over `querySelector`. It's not a performance bottleneck, and yields the same result (either `null` or an HTMLElement). The only difference is whether your string includes `#` or not. The querySelector and querySelectorAll functions are not "for classes", they're "for query selectors", which includes ids, tagnames, classes, attributes, and everything else a valid query selector may contain, and using them instead of getElementById, getElementsByTagName, etc. is perfectly fine. – Mike 'Pomax' Kamermans Aug 31 '21 at 19:26
  • 1
    @Jürgen Fink I saw your answer after I posted, that's where the +1 came from.. me, cheers. – norcal johnny Aug 31 '21 at 20:05
  • @Mike'Pomax'Kamermans You got an up-vote from me - I was not aware of that concerning `querySelector()`. For what reason is there `getElementById()` then? No performance improvement at all? – Jürgen Fink Aug 31 '21 at 20:59
  • 1
    History. `getElementById` predates `querySelector` by almost a decade, and querySelector didn't even get added until jQuery had showed how much more useful it was to be able to get elements by their CSS query selector for many years. – Mike 'Pomax' Kamermans Aug 31 '21 at 21:11
  • 1
    @Mike'Pomax'Kamermans I was able to understand what the guidelines suggested in writing an article were. Thanks. – Minwoo Kim Sep 01 '21 at 01:14

1 Answers1

1

Some issues I've found and would like to share:

I've noticed that in your JS file you are using repeatedly the

document.querySelector() (for classes)
instead of
document.getElementById() (for id)

Like for example:

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

I made all modifications within your code where appropiate and now code snippet runs at least (replaced src of video to a public video link for testing):

Edit, taking helpful comment into account:
I added to video tag following:

  1. added #t=0.1 to source of video tag to accelerate the poster of video at beginning according to my post at Dynamically using the first frame as poster in HTML5 video
  2. added type="video/mp4"
  3. added preload="auto"
    This helped to load poster at first click of "Load"

Hence we have now:

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

Just press "Load" and the posters should appear immediately:

// 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>
Jürgen Fink
  • 3,162
  • 2
  • 23
  • 25
  • 1
    The `Load` function was a poster function to show the first frame before `Play` the video. Currently it takes at least 2 clicks to load the first frame, is there any additional way to fix this? – Minwoo Kim Sep 01 '21 at 01:17
  • @MinwooKim I edited code in my answer, adding `#t=0.1` to source of ` – Jürgen Fink Sep 01 '21 at 15:20
  • 1
    @MinwooKim also added `preload="auto"` to ` – Jürgen Fink Sep 01 '21 at 15:40
  • 1
    I was able to find and understand the `preload` and `#t` options you mentioned. Thanks for sharing good knowledge. – Minwoo Kim Sep 02 '21 at 01:08