Play HEVC/H.265 video in any browser. No plugin. No install. No server changes.
A from-scratch HEVC decoder written in C++17, compiled to WebAssembly, with a drop-in plugin for dash.js. The browser only supports H.264? We transcode HEVC to H.264 in real-time, client-side, inside a Web Worker.
1080p @ 60fps. 236KB WASM. Zero dependencies. No special server headers required.
Built in 8 days by one developer, assisted by AI — read the story.
npm install @hevcjs/dashjs-pluginimport dashjs from 'dashjs';
import { attachHevcSupport } from '@hevcjs/dashjs-plugin';
const player = dashjs.MediaPlayer().create();
attachHevcSupport(player, { workerUrl: './transcode-worker.js' });
player.initialize(videoElement, 'https://example.com/manifest.mpd', true);-
MSE intercept — Patches
MediaSource.addSourceBuffer()before the player initializes. When the player creates an HEVC SourceBuffer, we return a proxy that accepts HEVC data but feeds H.264 to the real SourceBuffer. -
Worker pipeline — All heavy work runs in a Web Worker:
- Demux: mp4box.js extracts raw HEVC NAL units from fMP4 segments
- Decode: WASM decoder produces YUV frames (spec-compliant, pixel-perfect)
- Encode: WebCodecs
VideoEncodercompresses to H.264 - Mux: Custom fMP4 muxer wraps H.264 in ISO BMFF with correct timestamps
-
Transparent to the player — The proxy reports
updating, firesupdatestart/updateendevents, and returns realbufferedranges. The player's buffer management, ABR logic, and seek handling work unmodified.
Tradeoff: the software fallback introduces 2-3s of startup latency on the first segment (vs instant playback with native hardware decode). Once buffered, playback is smooth. When native HEVC is available, hevc.js detects it and does nothing.
~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+ | Yes (hardware) | No — bypassed | — |
| Chrome/Edge 107+ (Win/Mac) | Yes (hardware GPU) | No — bypassed | — |
| Chrome 94-106 (all platforms) | No | Yes | Yes (WebCodecs H.264) |
| Chrome < 94 | No | Yes | No (no WebCodecs) — falls back to AVC |
| Firefox 137+ (Windows) | Partial (hardware) | No — bypassed | — |
| Firefox (Linux, older) | No | Yes | No — Firefox H.264 encoding broken (Bug 1918769), falls back to AVC |
| Linux (Chrome, no VAAPI) | No | Yes | Yes (software encode) |
Other requirements (supported by all modern browsers):
- WebAssembly
- Web Workers
- Secure Context (HTTPS or localhost)
No Cross-Origin-Embedder-Policy or Cross-Origin-Opener-Policy headers needed — the WASM decoder is single-threaded and doesn't use SharedArrayBuffer. Works on any static file server.
#include "wasm/hevc_api.h"
HEVCDecoder* dec = hevc_decoder_create();
hevc_decoder_decode(dec, data, size);
int count = hevc_decoder_get_frame_count(dec);
for (int i = 0; i < count; i++) {
HEVCFrame frame;
hevc_decoder_get_frame(dec, i, &frame);
// frame.y / frame.cb / frame.cr — YUV planes (uint16_t*)
// frame.width / frame.height — luma dimensions
// frame.bit_depth — 8 or 10
}
hevc_decoder_destroy(dec);// Lifecycle
HEVCDecoder* hevc_decoder_create(void);
void hevc_decoder_destroy(HEVCDecoder* dec);
// Decode a complete HEVC bitstream (Annex B format)
int hevc_decoder_decode(HEVCDecoder* dec, const uint8_t* data, size_t size);
// Incremental decode (feed NAL units progressively)
int hevc_decoder_feed(HEVCDecoder* dec, const uint8_t* data, size_t size);
int hevc_decoder_drain(HEVCDecoder* dec);
// Access decoded frames (display order)
int hevc_decoder_get_frame_count(HEVCDecoder* dec);
int hevc_decoder_get_frame(HEVCDecoder* dec, int index, HEVCFrame* frame);| HEVCFrame field | Type | Description |
|---|---|---|
y, cb, cr |
const uint16_t* |
YUV plane pointers |
width, height |
int |
Luma dimensions (conformance window applied) |
stride_y, stride_c |
int |
Plane strides in samples |
bit_depth |
int |
8 or 10 |
poc |
int |
Picture Order Count (display order) |
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest --output-on-failure # 128 testsRequires Emscripten SDK.
source ~/emsdk/emsdk_env.sh
emcmake cmake -B build-wasm -DBUILD_WASM=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm
# Output: build-wasm/hevc-decode.js + hevc-decode.wasm (236KB)Single-threaded, Apple Silicon (M-series):
| Native C++ | WASM (Chrome) | |
|---|---|---|
| 1080p decode | 76 fps | 61 fps |
| 4K decode | 28 fps | 21 fps |
| 1080p transcode | — | ~2.5x realtime (6s segment in 2.4s) |
The WASM decoder is within 20% of native C++ performance, and reaches 83% of libde265 speed (a mature, 10-year-old optimized HEVC decoder) when both are compiled to WASM.
Implemented per ITU-T H.265 (v8, 08/2021) — 716 pages, transcribed directly from the spec. Validated pixel-perfect against ffmpeg on 128 test bitstreams.
| Feature | Status |
|---|---|
| CABAC arithmetic decoding (§9.3) | Complete |
| 35 intra prediction modes (§8.4) | Complete |
| Inter prediction — merge, AMVP, TMVP (§8.5) | Complete |
| 8-tap luma / 4-tap chroma interpolation (§8.5.3) | Complete |
| Weighted prediction — default + explicit (§8.5.3.3) | Complete |
| Inverse transform — DCT 4-32, DST 4 (§8.6) | Complete |
| Scaling lists (§8.6.3) | Complete |
| Deblocking filter (§8.7.2) | Complete |
| SAO — edge + band offset (§8.7.3) | Complete |
| 10-bit decoding (Main 10 profile) | Complete |
| Multi-slice (dependent + independent) | Complete |
| Tiles | Parsed + sequential decode |
| WPP (Wavefront Parallel Processing) | Complete |
hevc.js/
├── src/ C++17 HEVC decoder (ITU-T H.265 spec-compliant)
│ ├── bitstream/ Annex B parsing, NAL units, RBSP, Exp-Golomb
│ ├── syntax/ VPS, SPS, PPS, slice header parsing
│ ├── decoding/ CABAC, coding tree, intra/inter prediction, transform
│ ├── filters/ Deblocking filter, SAO
│ ├── common/ Types, Picture buffer, thread pool
│ └── wasm/ C API, Emscripten bindings
│
├── packages/
│ ├── core/ @hevcjs/core — WASM decoder + transcoding pipeline
│ └── dashjs-plugin/ @hevcjs/dashjs-plugin — dash.js plugin
│
├── demo/ Browser demos (DASH)
└── tests/ Unit tests + 128 oracle tests (pixel-perfect vs ffmpeg)
Live demos — try each plugin in your browser:
| Demo | Description |
|---|---|
| Decoder | Raw WASM decoder — drop a .265 file, frame-by-frame playback |
| dash.js | HEVC DASH streams via dash.js + WASM transcoding |
Each demo includes a "Force transcoding" toggle to bypass native HEVC detection — useful for testing the WASM pipeline on browsers that already support HEVC.
pnpm install
pnpm build:demo # Builds WASM + JS bundles + copies assets
npx serve demo # Open http://localhost:3000MIT — see LICENSE.
HEVC/H.265 may be covered by patents managed by Access Advance and other patent pools. This software is an independent implementation and does not include or grant any patent license. Users are responsible for evaluating patent obligations in their jurisdiction and use case.
Media samples use Big Buck Bunny (CC-BY 3.0, Blender Foundation). See THIRD-PARTY-NOTICES.md for full attribution.