Search code examples
javascripthtmlcssbootstrap-5

Synchronise HTML Videos


Problem: I am building 3 a column layout, where some columns must stay sticky to the top of the viewport. There is a section where I want to place the same video as background for all 3 columns and I want to achieve following effect: Leave middle column 'sticky-top' and allow the left and right column to scroll up.

I already tried a solution with <Canvas>, but there are a couple of problems. Canvases can not render frames without a blurry effect and it doesn't look good in webkit browsers (for firefox it seems to work better). Here is *(in case jsFiddle doesn't play video correctly, probably you need to insert Bootstrap's CDN links) a sample solution I am aiming for. I have tried to insert videos in the left and right column and adjust margins so that all three videos give feeling if they were one whole, but there I have another problem. On first run all videos play almost synchronously, but after while they flow out from sync, which looses effect of wholeness. Here is Solution with 3 background videos.

 ```
<div class="row g-0">
          <div class="o-cont-m o-col-3-c container-left">
            <canvas id="canvas-left" class="o-cont-m"></canvas>
          </div>
          <div class="o-cont-m o-col-3-c container-middle sticky-top">
             <video id="video-bg-middle" loop muted autoplay playsinline>
        <!--             <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4"> -->
               <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" type="video/mp4">
        <!--               <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" type="video/mp4"> -->
        <!--                <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4" type="video/mp4"> -->
        <!--                <source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" type="video/mp4"> -->
        <!--                       <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4" type="video/mp4"> -->
                    Your browser does not support the video tag.s
                </video>
          </div>
          <div class="o-cont-m o-col-4-c container-right">
            <canvas id="canvas-right" class="o-cont-m"></canvas>
          </div>
          <div class="o-cont-m o-col-3-c" style="background-color:grey"></div>
          <div class="o-cont-m o-col-3-c col-transparent sticky-top"></div>
          <div class="o-cont-m o-col-4-c" style="background-color:grey"></div>
          <div class="o-cont-m o-col-3-c" style="background-color:tomato"></div>
          <div class="o-cont-m o-col-3-c col-transparent sticky-top" ></div>
          <div class="o-cont-m o-col-4-c" style="background-color:tomato

"></div>
</div>

    ```
  

     .o-col-3-c {
      flex:0 0 auto;
      width:30%;
     }
    
    .o-col-4-c {
      flex:0 0 auto;
      width:40%;
    }
    
    .o-cont-m {
      block-size:100vh;
    }
    
    .col-transparent{
      background: rgba(0, 0, 0, .5);
    }
    video{
      width:100vw;
      height:100%;
      display:block;
      
      object-fit:fill;
    }
    
    .container-left,
    .container-middle,
    .container-right {
    overflow:hidden;
    }
    
    #canvas-left,
    #canvas-right {
    width: 100vw;
    height: 100vh;
    display: block;
    }

```
const middleCol = document.querySelector('.container-middle');
const middleColOffsetLeft = middleCol.offsetLeft;
console.log(middleColOffsetLeft);
const rightCol = document.querySelector('.container-right');
const rightColOffsetLeft = rightCol.offsetLeft;
console.log(rightColOffsetLeft);

const bgVideo = document.getElementById('video-bg-middle');
bgVideo.style.marginLeft = `-${middleColOffsetLeft}px`

const canvasLeft = document.getElementById("canvas-left");
const canvasRight = document.getElementById("canvas-right");
canvasRight.style.marginLeft = `-${rightColOffsetLeft}px`;
const ctx = canvasLeft.getContext("2d");
const ctx1 = canvasRight.getContext("2d");

function drawFrames() {
    if (!bgVideo.paused && !bgVideo.ended) {
        ctx.drawImage(bgVideo, 0, 0, canvasLeft.width, canvasLeft.height);
        ctx1.drawImage(bgVideo, 0, 0, canvasRight.width, canvasRight.height);
    }
    requestAnimationFrame(drawFrames); // Call drawFrames again on the next animation frame
}

bgVideo.addEventListener('play', () => {
  drawFrames();
});


    ```
     <!--THIS IS HTML FOR SOLUTION WITH VIDEOS-->
    <div class="row g-0">
      <div class="o-cont-m o-col-3-c container-left overfllow-hidden">
        <video id="video-bg-left" loop muted autoplay playsinline>
           <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" type="video/mp4">
                Your browser does not support the video tag.s
            </video>
      </div>
      <div class="o-cont-m o-col-3-c container-middle sticky-top overfllow-hidden">
         <video id="video-bg-middle" loop muted autoplay playsinline>
           <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" type="video/mp4">
                Your browser does not support the video tag.s
            </video>
      </div>
      <div class="o-cont-m o-col-4-c container-right overfllow-hidden">
        <video id="video-bg-right" loop muted autoplay playsinline>
           <source src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" type="video/mp4">
                Your browser does not support the video tag.s
            </video>
      </div>
      <div class="o-cont-m o-col-3-c" style="background-color:grey"></div>
      <div class="o-cont-m o-col-3-c col-transparent sticky-top"></div>
      <div class="o-cont-m o-col-4-c" style="background-color:grey"></div>
      <div class="o-cont-m o-col-3-c" style="background-color:tomato"></div>
      <div class="o-cont-m o-col-3-c col-transparent sticky-top" ></div>
      <div class="o-cont-m o-col-4-c" style="background-color:tomato"></div>
    </div>

````
/*CSS STAYS SAME*/
.o-col-3-c {
  flex:0 0 auto;
  width:30%;
}

.o-col-4-c {
  flex:0 0 auto;
  width:40%;
}

.o-cont-m {
  block-size:100vh;
}

.col-transparent{
  background: rgba(0, 0, 0, .5);
}
video{
  width:100vw;
  height:100%;
  display:block;
  
  object-fit:fill;
}

.container-left,
.container-middle,
.container-right {
overflow:hidden;
}



#video-bg-left,
#video-bg-middle {
width: 100vw;
height: 100vh;
display: block;
}

    ```
    /*JS FOR SOLUTION WITH  3 DIFFERENT VIDEOS*/
    const updateLeftMargin = () => {
        const middleCol = document.querySelector('.container-middle');
        const rightCol = document.querySelector('.container-right');
        const middleColOffsetLeft = middleCol.offsetLeft;
        const rightColOffsetLeft = rightCol.offsetLeft;
    
        const updateBackground = (elementId, marginLeft, width) => {
            const element = document.getElementById(elementId);
            if (element) {
                if (marginLeft !== null) {
                    element.style.marginLeft = marginLeft < 0 ? `${marginLeft}px` : `-${marginLeft}px`;
                }
                if (width !== null) {
                    element.style.width = `${width}px`;
                }
            }
        };
    
        updateBackground('video-bg-middle', middleColOffsetLeft, null);
        updateBackground('video-bg-right', -rightColOffsetLeft, null);
    };
    
    updateLeftMargin();
    
    window.addEventListener('resize', updateLeftMargin);

Solution

  • Canvases can actually do the job. The blurry effect will appear if the canvas got initialized with a smaller size than the video, so increasing the size of the canvas (e.g. width="1920" height="1080" for HD) will also sharpen the appearance. The canvas might not look 100% identical to the <video> so we can do a workaround by hidding the visibility of the video and replace it with an additional <canvas>. By drawing to all those canvases at a time there also won't be any issue regarding sync.

    Note: my css is not perfect but the left part is showing it pretty well

    const video = document.getElementById('video');
    
    const canvas = document.querySelectorAll('canvas');
    const canvasContexts = [];
    canvas.forEach(canvas => canvasContexts.push(canvas.getContext(
      '2d', { alpha: false }
    )));
    
    function draw() {
      canvasContexts.forEach(context => {
        context.drawImage(video, 0, 0, context.canvas.width, context.canvas.height);
      });
    
      if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
        video.requestVideoFrameCallback(() => draw());
      } else {
        window.requestAnimationFrame(() => draw());
      }
    }
    
    video.addEventListener('play', () => {
      draw();
    });
    body {
        margin: 0;
    }
    
    canvas {
        block-size: 100%;
        inline-size: 100vw;
    }
    
    .canvas-right {
        translate: -59.5%;
    }
    
    .overflow-hidden {
        overflow: hidden;
    }
    
    .hidden-video {
        position: fixed;
        visibility: hidden;
    }
    
    .video-wrapper {
        position: fixed;
        inline-size: 100%;
        z-index: -1
    }
    
    .row {
        display: flex;
    }
    
    .col-3 {
        inline-size: 30%;
    }
    
    .col-4 {
        inline-size: 40%;
    }
    
    .div-filler {
        block-size: 100vh;
        background: purple;
    }
    <main>
        <div style="position: relative">
            <div class="video-wrapper">
                <video class="hidden-video" id="video" muted autoplay loop>
                    <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4">
                </video>
                <canvas width="1920" height="1080"></canvas>
            </div>
            <div class="row">
                <div class="col-3 overflow-hidden">
                    <div>
                        <canvas class="canvas-left" width="1920" height="1080"></canvas>
                    </div>
                    <div class="div-filler"></div>
                </div>
                <div class="col-3">
    
                </div>
                <div class="col-4 overflow-hidden">
                    <div>
                        <canvas class="canvas-right" width="1920" height="1080"></canvas>
                        <div class="div-filler"></div>
                    </div>
                </div>
            </div>
        </div>
    </main>