HTMLVideoElement for Web Workers.
Live demo ·
npm package
npm install html-video-element-webworker-polyfillmain.js
import { createVideoHost } from "html-video-element-webworker-polyfill/host";
const worker = new Worker("./worker.js", { type: "module" });
createVideoHost(worker);
const canvas = document.querySelector("canvas").transferControlToOffscreen();
worker.postMessage({ canvas }, [canvas]);worker.js
import { polyfillDocument } from "html-video-element-webworker-polyfill/worker";
import { polyfill2dCanvasApi } from "html-video-element-webworker-polyfill/canvas2d";
polyfillDocument();
polyfill2dCanvasApi();
self.addEventListener("message", ({ data }) => {
if (!data.canvas) return;
const ctx = data.canvas.getContext("2d");
const video = document.createElement("video");
video.muted = true;
video.src = "movie.mp4";
video.play();
video.requestVideoFrameCallback(function draw() {
ctx.drawImage(video, 0, 0, ctx.canvas.width, ctx.canvas.height);
video.requestVideoFrameCallback(draw);
});
});index.html
<video id="player" src="movie.mp4"></video>main.js
import { createVideoHost } from "html-video-element-webworker-polyfill/host";
createVideoHost(new Worker("./worker.js", { type: "module" }));worker.js
import { connectToHostVideo } from "html-video-element-webworker-polyfill/worker";
const video = await connectToHostVideo("#player");
video.currentTime = 42;main.js
import { createVideoHost } from "html-video-element-webworker-polyfill/host";
createVideoHost(new Worker("./worker.js", { type: "module" }), {
onVideoCreated(video) {
if (video.id === "preview") document.body.append(video);
},
});worker.js
import { polyfillDocument } from "html-video-element-webworker-polyfill/worker";
polyfillDocument();
const video = document.createElement("video");
video.id = "preview";
video.muted = true;
video.src = "movie.mp4";
video.play();CanvasVideoElement has the HTMLVideoElement API but decodes with WebCodecs onto its own canvas, so it
needs no host and works the same in a worker and on the main thread. One decoder stays warm across seeks: setting
currentTime to the next frame runs at the speed of the decoder itself, and seeking does not wait for a visible tab.
MP4 and WebM are demuxed by Mediabunny from HTTP byte ranges, so only the bytes that are needed are
downloaded.
import { CanvasVideoElement } from "html-video-element-webworker-polyfill/canvas-video";
const video = new CanvasVideoElement({ canvas: offscreenCanvas });
video.src = "movie.mp4";
await new Promise((resolve) => video.addEventListener("loadeddata", resolve, { once: true }));
for (let i = 0; i < frameCount; i++) {
video.currentTime = i / fps;
await new Promise((resolve) => video.addEventListener("seeked", resolve, { once: true }));
process(video.currentFrame);
}video.currentFrame is the VideoFrame on screen; it belongs to the element, so clone() it to keep it.
play() plays with sound through Web Audio. In a worker, create the audio output on the main thread and pass its port:
import { createAudioOutput } from "html-video-element-webworker-polyfill/canvas-video";
const audio = createAudioOutput();
worker.postMessage({ audio: audio.port }, [audio.port]);const video = new CanvasVideoElement({ audio: data.audio });WebCodecs gives you a decoder, not a player: you demux containers yourself, configure codecs, feed chunks
and find keyframes to seek. HTMLVideoElement already handles every format the browser can play and seeks
frame-accurately. It just does not exist in workers. This package makes it available there.
| Import | Exports |
|---|---|
html-video-element-webworker-polyfill/host |
createVideoHost(worker, { container?, onVideoCreated?, canConnect? }) — main thread |
html-video-element-webworker-polyfill/worker |
createVideoElement(), connectToHostVideo(selector), polyfillDocument() (document.createElement), WorkerVideoElement |
html-video-element-webworker-polyfill/canvas2d |
polyfill2dCanvasApi() — drawImage, createPattern |
html-video-element-webworker-polyfill/webgl |
polyfillWebglAPI() — texImage2D and friends, WebGL 1 and 2 |
html-video-element-webworker-polyfill/webgpu |
polyfillWebgpu() — copyExternalImageToTexture, importExternalTexture |
html-video-element-webworker-polyfill/image-bitmap |
polyfillCreateImageBitmap() — createImageBitmap, polyfillVideoFrame() — new VideoFrame |
html-video-element-webworker-polyfill/auto |
installs every worker polyfill |
html-video-element-webworker-polyfill/canvas-video |
CanvasVideoElement({ canvas?, audio?, readAhead?, frameQueueSize?, hardwareAcceleration?, requestInit? }), createAudioOutput() |
The worker element implements the HTMLVideoElement API, plus currentFrame (the
VideoFrame on screen) and dispose().
- Cross-origin media needs CORS.
- The package shares the worker's
postMessagechannel with your code, so check your own messages, as in example 1. - Requires
VideoFrameandrequestVideoFrameCallback: Chrome/Edge 94+, Firefox 132+, Safari 17+. CanvasVideoElementplays only the codecs the browser's WebCodecs decoders support, and changes pitch withplaybackRate(preservesPitchis not supported).
Apache-2.0