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.
Without a canvas option the element creates its own canvas the first time video.canvas is read, and until then
draws no frames, so reading currentFrame alone costs no extra copy.
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