html-video-element-webworker-polyfill

HTMLVideoElement in a Web Worker

Create and control a <video> from a Web Worker and read its decoded frames there. Any format the browser can play, no demuxing, and the main thread stays free.

More examples: manage an existing video · create and show a video

Live demo

Main thread The real <video> Created, played and seeked by the worker.
Web Workers Frames processed in the workers Each worker pulls its own frames and filters them pixel by pixel in JS.
1 main thread –frames/s Switch to “Main thread” to measure.
4 workers –frames/s Measuring…

Frames processed per second, summed over all videos. Same videos, same filter and the same extra work in both cases.

0:00 / 0:00
Process frames on

Videos and worker threads

Extra CPU work per frame: 25 ms

Simulates a heavy per-frame algorithm.

Filter
Video
Main thread smoothness –

Each bar is one main-thread animation frame. Tall red bars are frames the page could not render in time.

Why not just WebCodecs?

WebCodecs gives you a decoder, not a player. HTMLVideoElement already does everything around it, and for frame processing that is usually exactly what you need. Its only problem is that it does not exist in workers. This package fixes that.

VideoDecoder (WebCodecs)

  • You demux every container yourself (mp4box.js, a WebM parser, …).
  • You configure codecs, feed chunks and manage decoder queues.
  • Seeking means finding keyframes and decoding forward to the target time.
  • Streaming, range requests and buffering are up to you.

<video> in a worker (this package)

  • Every format the browser plays: MP4, WebM, MOV, MSE streams, HLS where supported.
  • No demuxing: set src, then play() or set currentTime.
  • Frame-accurate seeked with the decoded frame ready in the handler.
  • Frames reach the worker as zero-copy VideoFrames for Canvas, WebGL or WebGPU.

How it works

The main thread keeps the real element. The worker gets an object with the same API: properties and calls are forwarded over postMessage, state and events come back, and every decoded frame is transferred as a VideoFrame. The worker keeps the current frame, so it can read it synchronously.

main.js

import { createVideoHost } from "html-video-element-webworker-polyfill/host";

const worker = new Worker("./worker.js", { type: "module" });
createVideoHost(worker);

worker.js

import { polyfillDocument } from "html-video-element-webworker-polyfill/worker";
import { polyfill2dCanvasApi } from "html-video-element-webworker-polyfill/canvas2d";

polyfillDocument();
polyfill2dCanvasApi();

const video = document.createElement("video");
video.src = "movie.mp4";
video.currentTime = 12;
video.addEventListener("seeked", () => {
  ctx.drawImage(video, 0, 0);
  const pixels = ctx.getImageData(0, 0, w, h);
});