Quick start
Install the package, register the transmuxer, load your stream.
$ npm install @hevcjs/shaka-plugin shaka-player
import shaka from 'shaka-player';
import { registerHevcTransmuxer } from '@hevcjs/shaka-plugin';
// Register once — Shaka routes hev1/hvc1 through the transmuxer
registerHevcTransmuxer(shaka);
const video = document.querySelector('video');
const player = new shaka.Player();
await player.attach(video);
await player.load('https://example.com/stream/manifest.mpd');
How it works
A native Shaka Transmuxer. Shaka handles MIME routing — the plugin only converts bytes.
shaka.transmuxer.TransmuxerEngine
— for video/mp4; codecs="hev1" and hvc1
isSupported / convertCodecs hooks
— Shaka handles HEVC MIME routing natively
SegmentTranscoder.prepareInit()
— returns a matching H.264 fMP4 init segment by warming up the encoder with a single black frame
transmux()
— demux, decode HEVC (WASM), encode H.264 (WebCodecs), mux fMP4, return to Shaka
workerUrl)
— runs the decode + encode pipeline off the main thread (recommended for 4K / high-bitrate)
player.configure({ mediaSource: { forceTransmux: true } }) routes HEVC through the transmuxer even where the browser supports HEVC natively
Browser compatibility
~94% of browsers play HEVC natively (hardware decode). hevc.js activates only for the ~6% that don't.
| Browser | Native HEVC | hevc.js needed? | Transcoding works? |
|---|---|---|---|
| Safari 13+ (macOS/iOS) | Yes (VideoToolbox) | No — native | — |
| Chrome/Edge/Firefox (Mac) | Yes (VideoToolbox) | No — native | — |
| Chrome 107+ (Win, HEVC-capable GPU) | Yes (D3D11VA) | No — native | — |
| Chrome 107+ (Win, GPU without HEVC) | No | Yes | Yes (WebCodecs H.264) |
| Edge (Win, with HEVC Video Extension) | Yes (MFT) | No — native | — |
| Edge (Win, no extension) | No | Yes | Yes (WebCodecs H.264) |
| Firefox 133+ (Win, with HEVC Video Extension) | Yes (MFT) | No — native | — |
| Firefox 133+ (Win, no extension — use forceTransmux) | Reported but fake | Yes | Yes (force via player.configure) |
| Chrome/Edge 94–106 | No | Yes | Yes (WebCodecs H.264) |
| Chrome/Edge < 94 | No | Yes | No (no WebCodecs) — falls back to AVC |
| Chrome (Linux, VAAPI) | Variable (driver-dependent) | Sometimes | Yes (software encode) |
| Chrome (Linux, no VAAPI) | No | Yes | Yes (software encode) |
| Firefox (Linux) | No | Yes | Depends — needs WebCodecs H.264 encoder |
Other requirements (supported by all modern browsers):
API reference
One function to register, one function to tear down.
registerHevcTransmuxer(shaka, config?)
Registers the Shaka transmuxer for HEVC mime types.
Returns a cleanup() function that unregisters it.
const cleanup = registerHevcTransmuxer(shaka, {
workerUrl: '/transcode-worker.js', // Web Worker URL
wasmUrl: '/hevc-decode.js', // WASM glue location
wasmBinaryUrl: '/hevc-decode.wasm', // WASM binary location
});
// Force the transmuxer even on browsers with native HEVC
player.configure({ mediaSource: { forceTransmux: true } });
// Unregister when done
cleanup();
Options
workerUrl
string
URL of the Web Worker script for off-main-thread transcoding. Recommended for 4K and high-bitrate streams. If omitted, transcoding runs on the main thread.
wasmUrl
string
Path to the WASM glue JS file. Auto-detected from the package if omitted.
wasmBinaryUrl
string
Path to the WASM binary. Useful when serving the decoder from a custom path or CDN. Forwarded to the underlying SegmentTranscoder.
forceTransmux
Shaka config
Not a plugin option — use Shaka's built-in player.configure({ mediaSource: { forceTransmux: true } }) to route HEVC through the transmuxer even when the browser supports HEVC natively.
adaptiveCompute — compute-aware ABR on by default
Feedback loop that caps Shaka variants when the device can't sustain transcoding at real-time. Shaka's bandwidth-based ABR keeps choosing freely — we only narrow the ceiling it's allowed to pick from.
Useful on devices where WASM HEVC decode + WebCodecs H.264 encode runs slower than playback (typical on low-end Windows boxes at 1080p+). On by default — pass adaptiveCompute: false to opt out.
// Default — adaptiveCompute is ON, defaults are sensible
const handle = registerHevcTransmuxer(shaka, {
workerUrl: '/transcode-worker.js',
});
// To tune knobs:
// registerHevcTransmuxer(shaka, { adaptiveCompute: { targetSpeedX: 1.5 } })
// To opt out:
// registerHevcTransmuxer(shaka, { adaptiveCompute: false })
const player = new shaka.Player();
const detach = handle.attachComputeAware(player); // after player exists
await player.load(manifestUrl);
// Cleanup
detach(); // stop observing
handle(); // unregister transmuxer + detach
Tuning knobs
targetSpeedX
number, default 1.3
Required headroom over real-time before the cap is allowed to rise. 1.3 means the rolling average must show transcode at 1.3× real-time before we let the host ABR pick a higher variant.
measureWindow
number, default 2
Rolling-window size for smoothing. Default is intentionally small — the cost of staying too long at an unreachable quality (buffer underrun, frozen playback) is much worse than the cost of a brief unnecessary dip.
lowerAfter
number, default 1
Consecutive windows averaging below 1.0× before lowering the cap one notch. Default 1 — react on the first confirmed low window.
raiseAfter
number, default 6
Consecutive windows averaging above targetSpeedX before raising the cap one notch. Wider than lowerAfter by design — hysteresis prevents yo-yo between two adjacent variants.
onObservation
(stat, avg, cap) => void
Optional telemetry sink called for every segment (not just cap changes). Handy for plotting speedX over time in a demo or admin panel.
Caps are applied via Shaka's public player.configure({ abr: { restrictions: { maxHeight, maxBandwidth } } }) — no fork of the ABR controller, no reach into private APIs.
Lower-level API
For advanced use cases — direct transcoding without Shaka, via @hevcjs/core.
import { SegmentTranscoder }
from '@hevcjs/core';
const transcoder = new SegmentTranscoder({ fps: 25 });
await transcoder.init();
// Synthesize a matching H.264 init segment up-front
const init = await transcoder.prepareInit(hevcInitBytes);
// Then transcode media segments
const h264Segment = await transcoder.processMediaSegment(mediaSegmentBytes);
Performance
Real numbers from a single-threaded WebAssembly decoder.
workerUrl to keep the pipeline off the main thread for 4K and high-bitrate streams.
FAQ
Does it work with live streams?
Yes. The transmuxer converts segments as Shaka requests them, so live (low-latency) and VOD streams both work. The only difference is that the first segment takes 2-3 seconds to transcode, which adds to the initial live edge latency.
What happens when native HEVC is available?
Shaka plays HEVC natively and the transmuxer stays out of the way — Safari, any browser on macOS, Chrome 107+ on Windows with an HEVC-capable GPU, or Edge/Firefox on Windows with the Microsoft HEVC Video Extension installed. To force transcoding anyway, use Shaka's built-in player.configure({ mediaSource: { forceTransmux: true } }).
Do I need special server headers?
No. The WASM decoder is single-threaded and does not use SharedArrayBuffer, so no Cross-Origin-Embedder-Policy or Cross-Origin-Opener-Policy headers are needed. It works on any static file server with HTTPS.
How is this different from the dash.js plugin?
The Shaka plugin registers a native Shaka Transmuxer for hev1/hvc1, so Shaka handles MIME routing itself. The dash.js plugin instead intercepts the MSE pipeline directly. Both share the same @hevcjs/core HEVC decoder and H.264 encoder.
Get started
Install the package and start playing HEVC in every browser.
npm install @hevcjs/shaka-plugin