@hevcjs/shaka-plugin

HEVC Plugin for Shaka Player

Play H.265 streams in every browser. One registration call.

Registers a Shaka Transmuxer that transcodes HEVC to H.264 client-side via WebAssembly.
When native HEVC is available, Shaka plays it directly.

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.

1
Registers a shaka.transmuxer.TransmuxerEngine — for video/mp4; codecs="hev1" and hvc1
2
Exposes the standard isSupported / convertCodecs hooks — Shaka handles HEVC MIME routing natively
3
On the init segment, calls SegmentTranscoder.prepareInit() — returns a matching H.264 fMP4 init segment by warming up the encoder with a single black frame
4
On each media segment, transmux() — demux, decode HEVC (WASM), encode H.264 (WebCodecs), mux fMP4, return to Shaka
5
Optional Web Worker (workerUrl) — runs the decode + encode pipeline off the main thread (recommended for 4K / high-bitrate)
6
Force transcoding when needed player.configure({ mediaSource: { forceTransmux: true } }) routes HEVC through the transmuxer even where the browser supports HEVC natively
Audio & subtitles pass through untouched. Only the HEVC video track is transcoded.

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):

WebAssembly Web Workers Secure Context (HTTPS) WebCodecs VideoEncoder shaka-player ≥ 4.0.0

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.

60fps
1080p decode
WASM, single-thread
236KB
WASM binary
gzipped, zero deps
2-3s
startup latency
first segment transcode
Tradeoff: the first segment takes 2-3s to transcode (vs instant with native hardware decode). Once buffered, playback is smooth. Use 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