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: