Implementing video mirroring on the Web

Implementing video mirroring on the Web

May 24, 2022·

3 min read

When we want to play a video either from video files or video streams, we usually use video tag in HTML. It's easy to use but no mirroring function is provided. If we want this, we need to implement it by ourself. Below let's discuss a few possible options.

CSS

Transform directive in CSS provides scale function, we can use this with the video element. The usage is simple, if we want mirroring horizotally, just use transform: scaleX(-1);. The following demonstrates the effect corresponding to the different parameters.

133875667-0a72a847-a85e-4fdd-a5e1-a596896ac0aa.png

As you can see, the purpose is achieved and the performance is good. But if we use controls with the video element, this will mirroring too, which is not good.

133874071-78f16b2a-6ea4-48e4-8b68-6956df635175.png

The last thing we need to know about this approach is old vendor prefixes.

-webkit-transform: scaleX(-1);
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
transform: scaleX(-1);

Canvas

Canvas also provides a scale function, we can use this to implement mirroring too.

<body>
  <button id="start">start</button>
  <div class="container">
    <div class="item">
      <video src="./moive.mp4" class="v-1" id="origin-v"></video>
    </div>
    <div class="item">
      <canvas width="480" height="320" id="canvas"></canvas>
    </div>
  </div>

  <script>
    const startBtn = document.getElementById("start")

    const video = document.getElementById("origin-v")
    const canvas = document.getElementById("canvas")
    const ctx = canvas.getContext("2d")

    // translate first, and then scale
    ctx.translate(480, 0)
    ctx.scale(-1, 1)

    let ratio;
    startBtn.onclick = () => {
      video.play();
      ratio = video.videoWidth / 480;
      draw()
    }

    function draw() {
      ctx.drawImage(video, 0, 0, 480, video.videoHeight / ratio);
      requestAnimationFrame(draw);
    }
  </script>
</body>

And the result looks the same.

133877953-a3a1f782-b23e-4f5f-a662-dda6df24ee75.png

But we should know that canvas takes more cpu than plain video tag, so that's kind of a downside.

More on canvas

Since we can get every pixel value using getImageData function in canvas, so we can achieve mirroring by manipulating raw pixel value.

<body>
  <button id="start">start</button>
  <div class="container">
    <div class="item">
      <video src="./moive.mp4" class="v-1" id="origin-v"></video>
    </div>
    <div class="item">
      <canvas width="480" height="320" id="canvas"></canvas>
    </div>
  </div>

  <script>
    const startBtn = document.getElementById("start")

    const video = document.getElementById("origin-v")
    const canvas = document.getElementById("canvas")
    const ctx = canvas.getContext("2d")

    const offScreenCanvas = new OffscreenCanvas(480, 320);
    const offScreenCtx = offScreenCanvas.getContext("2d");

    let ratio;
    startBtn.onclick = () => {
      video.play();
      ratio = video.videoWidth / 480;
      draw()
    }

    function draw() {
      const width = 480;
      const height = video.videoHeight / ratio;
      offScreenCtx.drawImage(video, 0, 0, width, height);

      // off screen canvas is used to get image data
      const imageData = offScreenCtx.getImageData(0, 0, width, height);

      // manipulate image data here to make mirroring effect
      const halfWidth = parseInt(imageData.width / 2);
      for (let i = 0; i < imageData.height; i++) {
        for (let j = 0; j < halfWidth; j++) {
          for (let k = 0; k < 4; k++) {
            const sourceIdx = i * imageData.width * 4 + (j * 4) + k;
            const targetIdx = i * imageData.width * 4 + (imageData.width * 4 - j * 4 - 4) + k;
            const temp = imageData.data[targetIdx];
            imageData.data[targetIdx] = imageData.data[sourceIdx];
            imageData.data[sourceIdx] = temp;
          }
        }
      }

      // draw new image data
      ctx.putImageData(imageData, 0, 0)
      requestAnimationFrame(draw);
    }
  </script>
</body>

We achieve the same result, but with more cpu burden.

133883407-1e6ef88b-9cf7-4c6d-ab85-5b2b2b40c0cc.png