hometools

A Media Source Extensions (MSE) Video Player

April 2026

Overview

The Video

Here's the output demo video to get started. It's from pexels which is a nice place to get free videos.

The JavaScript

The main JavaScript consists of a variable and two functions in this module:

media-source-player.js
const videos = {};

export async function loadVideo(selector, config) {
  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 {
    el.src = config.fallback;
  }
}

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();
        videos[uuid].sourceBuffer.appendBuffer(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();
  }
}

The HTML

Here's the HTML I'm using on the page:

<video id="videoPlayer" controls></video>

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

The Prep Script

The sample video I used didn't have an audio track with it. It worked fine in Safari and Firefox. It broke in chrome. It took a while to figure out that the missing audio was the problem.

This is the ffmpeg command I used to fix the issue. It makes a new .webm file with a silent audio track.

audio-fix.bash
#!/bin/bash

ffmpeg -y -f lavfi -i anullsrc -i INPUT.webm -c:v copy -c:a libvorbis -shortest OUTPUT.webm
The resulting file is encoded so that it matches the `video/webm; codecs="vp9, vorbis"` in the JavaScript.

NOTE: I updated ffmpeg an hour after writing this post and libvorbis didn't get installed with the new version. I don't want to compile ffmpeg myself so I'll see about using libopus or something else instead next time I have to mess with stuff.

The Split Script

Spitting the source .webm file into the parts is done with this script that runs in bash on mac/linux (I don't have a Windows machine handy to determine the command on that OS).

split-video.bash
#!/bin/bash

split -b 1M -d "church-3818213.webm" part-
for f in part-*; do mv "$f" "$f.part"; done
Line 3 does the split.

Line 4 is optional. It adds file extensions. Some servers require one to server the proper mine time. It's not necessary on mine. I just like having explicit extensions.

Error Messages

Here's some of the Chrome console log error messages I got before figuring out that the audio track was the problem. Putting them here in hopes that folks searching for them will see these instead of being sent down dead ends by AI that doesn't take into account that the video could be the problem.

MediaSource readyState is: closed

MediaSource or SourceBuffer invalid

InvalidStateError: Failed to execute 
'appendBuffer' on 'SourceBuffer': 
This SourceBuffer has been removed 
from the parent media source.

InvalidStateError: Failed to execute 
'endOfStream' on 'MediaSource': 
The MediaSource's readyState is 
not 'open'.

--- From ffmpeg during processing

Error parsing Opus packet header.

Wrap Up

This is the first time I've really looked at web video in the modern age. As with so much these days, it's mind-blowing what you can do with it without having to bend over backwards. It took me way longer to figure out that the audio track was causing issues than to write up the player script.

Very cool.

-a

References