@hevcjs/dashjs-plugin

HEVC Plugin for dash.js

Play H.265 streams in every browser. 3 lines of code.

Transparently transcodes HEVC to H.264 client-side via WebAssembly.
When native HEVC is available, the plugin does nothing.

Quick start

Install the package, import one function, call it on your dash.js player.

$ npm install @hevcjs/dashjs-plugin dashjs
import dashjs from 'dashjs';
import { attachHevcSupport } from '@hevcjs/dashjs-plugin';

const video = document.querySelector('video');
const player = dashjs.MediaPlayer().create();

// One line — that's it
attachHevcSupport(player);

player.initialize(video, 'https://example.com/stream/manifest.mpd', true);

How it works

The plugin intercepts the MSE pipeline. The player never knows HEVC was involved.

1
Patches MediaSource.isTypeSupported() — returns true for HEVC codecs (hev1/hvc1)
2
Patches navigator.mediaCapabilities.decodingInfo() — same, for dash.js 4.x+
3
Registers a custom capabilities filter — accepts HEVC representations in the manifest
4
Intercepts addSourceBuffer() — creates an H.264 SourceBuffer and returns a Proxy
5
Proxy intercepts appendBuffer() — demux, decode HEVC (WASM), encode H.264 (WebCodecs), mux fMP4, append to real SourceBuffer
6
Proper updating state management — the proxy reports updating = true during transcoding, so dash.js waits between segments
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 — bypassed
Chrome/Edge/Firefox (Mac) Yes (VideoToolbox) No — bypassed
Chrome 107+ (Win, HEVC-capable GPU) Yes (D3D11VA) No — bypassed
Chrome 107+ (Win, GPU without HEVC) No Yes Yes (WebCodecs H.264)
Edge (Win, with HEVC Video Extension) Yes (MFT) No — bypassed
Edge (Win, no extension) No Yes Yes (WebCodecs H.264)
Firefox 133+ (Win, with HEVC Video Extension) Yes (MFT) No — bypassed
Firefox 133+ (Win, no extension) Reported but fake Yes Yes (SourceBuffer probe catches false positive)
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

API reference

One function to set up, one function to tear down.

attachHevcSupport(player, config?)

Patches the browser APIs and registers the dash.js capabilities filter. Returns a cleanup() function that reverses all patches.

const cleanup = attachHevcSupport(player, {
  workerUrl: '/transcode-worker.js',   // Web Worker URL
  wasmUrl:   '/hevc-decode.js',         // WASM glue location
  fps:       25,                        // Target framerate
  bitrate:   4_000_000,                 // H.264 encode bitrate
});

// Remove all patches when done
cleanup();

Options

workerUrl string

URL of the Web Worker script for off-main-thread transcoding. If omitted, transcoding runs on the main thread.

wasmUrl string

Path to the WASM glue JS file. Auto-detected from the package if omitted.

fps number default: 25

Target framerate for H.264 encoding. Should match the source stream.

bitrate number

H.264 encode bitrate in bits/second. If omitted, WebCodecs chooses automatically.

adaptiveCompute — compute-aware ABR on by default

Feedback loop that caps dash.js variants when the device can't sustain transcoding at real-time. dash.js's bandwidth-based ABR keeps picking freely — we only narrow the bitrate ceiling. Unlike Shaka, the player is available at registration time, so no extra attach() call is needed. On by default — pass adaptiveCompute: false to opt out.

// Default — adaptiveCompute is ON, defaults are sensible
const cleanup = await attachHevcSupport(player, {
  workerUrl: '/transcode-worker.js',
});

// To tune knobs:
// attachHevcSupport(player, { adaptiveCompute: { targetSpeedX: 1.5 } })
// To opt out:
// attachHevcSupport(player, { adaptiveCompute: false })

player.initialize(videoElement, mpdUrl, true);

// Cleanup detaches the compute-aware listener AND uninstalls MSE intercept
cleanup();

Tuning knobs

targetSpeedX number, default 1.3

Required headroom over real-time before the cap is allowed to rise.

measureWindow number, default 2

Rolling-window size for smoothing per-segment speedX. Small by design — react fast when the device can't keep up.

lowerAfter number, default 1

Consecutive low windows before lowering the cap one notch.

raiseAfter number, default 6

Consecutive high windows before raising the cap one notch. Wider than lowerAfter — hysteresis prevents yo-yo.

Caps are applied via player.updateSettings({ streaming: { abr: { maxBitrate: { video } } } }) — dash.js's public ABR settings, no fork.

Lower-level API

For advanced use cases — manual MSE patching or direct transcoding without dash.js.

import { installMSEIntercept, uninstallMSEIntercept }
  from '@hevcjs/dashjs-plugin';
import { SegmentTranscoder }
  from '@hevcjs/dashjs-plugin';

// Manual MSE patching (without dash.js)
installMSEIntercept({ wasmUrl: '/hevc-decode.js' });

// Or use the transcoder directly
const transcoder = new SegmentTranscoder({ fps: 25 });
await transcoder.init();
await transcoder.processInitSegment(initSegmentBytes);
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. When native HEVC is available, the plugin detects it and does nothing — zero overhead.

FAQ

Does it work with live streams?

Yes. The plugin intercepts segments as they arrive, 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?

Nothing. The plugin checks for native HEVC support at startup and stays dormant when the browser can play HEVC directly — Safari, any browser on macOS, Chrome 107+ on Windows with an HEVC-capable GPU (Intel Skylake 2015+, NVIDIA Maxwell 2nd gen 2015+, AMD Fiji 2015+), or Edge/Firefox on Windows with the Microsoft HEVC Video Extension installed. Zero overhead.

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.

What about HLS?

There is no hls.js plugin at this time. The MSE intercept pattern works the same way, but hls.js support has not been implemented yet. dash.js is supported via @hevcjs/dashjs-plugin, and Shaka Player via @hevcjs/shaka-plugin.

Get started

Install the package and start playing HEVC in every browser.

npm install @hevcjs/dashjs-plugin