hometools

An Encrypted Media Source Extensions (MSE) Video Player

April 2026

Overview

The Video

Here's the video which is from pexels.

The JavaScript

This is the module that makes things work:

encrypted-media-source-player.js
import init, { decrypt_file } from "./pkg/static_site_file_decryption.js";

let initialized = false;
const videos = {};

export async function loadEncryptedVideo(selector, config) {
  if (initialized === false) {
    await init();
    initialized = true;
  }
  const uuid = self.crypto.randomUUID();
  const el = document.querySelector(selector);
  el.addEventListener("canplaythrough", () => {
    console.log("Got canplaythrough.");
  });
  const mediaSource = new MediaSource();
  videos[uuid] = {
    mediaSource: mediaSource,
    urls: config.urls,
    currentSegment: 0,
  };
  if (mediaSource) {
    el.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener("sourceopen", async () => {
      videos[uuid].sourceBuffer = mediaSource.addSourceBuffer(config.codec);
      videos[uuid].sourceBuffer.addEventListener("updateend", async (event) => {
        if (videos[uuid].mediaSource.readyState === "open") {
          loadSegment(uuid);
        }
      });
      loadSegment(uuid);
    });
  } else {
    try {
      const response = await fetch(config.fallback);
      if (response.ok) {
        const buffer = await response.arrayBuffer();
        const bytes = new Uint8Array(buffer);
        const responseBytes = await decrypt_file(bytes, "key");
        const blob = new Blob([responseBytes], { type: "video/webm" });
        const url = URL.createObjectURL(blob);
        el.src = url;
      } else {
        console.error(response);
      }
    } catch (error) {
      console.error(error);
    }
  }
}

async function loadSegment(uuid) {
  if (videos[uuid].currentSegment < videos[uuid].urls.length) {
    try {
      const url = videos[uuid].urls[videos[uuid].currentSegment];
      const response = await fetch(url);
      if (response.ok) {
        console.log(`processing segment: ${videos[uuid].currentSegment}`);
        videos[uuid].currentSegment++;
        const buffer = await response.arrayBuffer();
        const bytes = new Uint8Array(buffer);
        const responseBytes = await decrypt_file(bytes, "key");
        videos[uuid].sourceBuffer.appendBuffer(responseBytes.buffer);
      } else {
        console.error(response);
        videos[uuid].mediaSource.endOfStream("network");
      }
    } catch (error) {
      console.error(error);
      videos[uuid].mediaSource.endOfStream("network");
    }
  } else {
    videos[uuid].mediaSource.endOfStream();
  }
}

It's basically the same thing as A Media Source Extensions MSE Video Player with with the WASM decryption module from Password Protecting Static Site Content with WASM thrown in.

The HTML

The code for the page looks like this:

page.html
<video id="videoPlayer" controls></video>

<script type="module">
import { loadEncryptedVideo } from "/javascript/encrypted-media-source-video-player/encrypted-media-source-player.js"
const config = {
  codec: `video/webm; codecs="vp9, vorbis"`,
  urls: [
    "/javascript/encrypted-media-source-video-player/sample-video/part-00.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-01.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-02.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-03.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-04.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-05.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-06.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-07.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-08.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-09.part.bin",
    "/javascript/encrypted-media-source-video-player/sample-video/part-10.part.bin",
  ],
  fallback: "/javascript/encrypted-media-source-video-player/sample-video/church-3818213.webm.bin",
}
loadEncryptedVideo("#videoPlayer", config);
</script>

Exactly the same structure as A Media Source Extensions MSE Video Player. Only the URLs changed.

NOTE: Each part-##.part.bin file has to be encrypted individually after the file spilt occurs. (That is, you can't encrypt the church-3818213.webm and split it. You have to split first then encrypt.)

Details on the video prep and file split are on A Media Source Extensions MSE Video Player.

Wrap Up

Server video used to feel like magic. Something that required more tech than I had access to. Of course, that wasn't true. We've got access to the same browser APIs as YouTube. What's cool here is that with the current APIs there's no need to have a dynamic server in order to deliver encrypted videos that are served piece by piece.

We live in the future.

-a

References

See these pages for more details and references: