@hevcjs/core
v1.3.1
Published
HEVC/H.265 video decoder for the browser using WebAssembly — decode HEVC streams directly in JavaScript with MSE support
Maintainers
Readme
@hevcjs/core
HEVC/H.265 decoder compiled to WebAssembly, with a built-in HEVC-to-H.264 transcoding pipeline. 236KB WASM, zero dependencies.
Implemented per ITU-T H.265 v8 — 716 pages of spec, validated pixel-perfect against ffmpeg on 128 test bitstreams.
Install
npm install @hevcjs/coreSetup
The package includes 3 static files that must be served by your web server:
transcode-worker.js— Web Worker (IIFE, standalone)wasm/hevc-decode.js— Emscripten glue codewasm/hevc-decode.wasm— WASM binary (236KB)
Copy them from node_modules/@hevcjs/core/dist/ to your public/static directory:
cp node_modules/@hevcjs/core/dist/transcode-worker.js public/
cp node_modules/@hevcjs/core/dist/wasm/hevc-decode.js public/
cp node_modules/@hevcjs/core/dist/wasm/hevc-decode.wasm public/Adjust the destination to match your project structure (public/, static/, dist/, etc.).
Usage
MSE intercept (transparent HEVC playback)
import { installMSEIntercept } from '@hevcjs/core';
installMSEIntercept({
workerUrl: '/transcode-worker.js',
wasmUrl: '/hevc-decode.js',
});Loading from a CDN (zero build, cross-origin)
@hevcjs/core works directly from a CDN — no install, no build step, no static asset copy:
<script type="module">
import { installMSEIntercept } from 'https://esm.sh/@hevcjs/core@1';
installMSEIntercept({
workerUrl: 'https://unpkg.com/@hevcjs/core@1/dist/transcode-worker.js',
wasmUrl: 'https://unpkg.com/@hevcjs/core@1/dist/wasm/hevc-decode.js',
wasmBinaryUrl: 'https://unpkg.com/@hevcjs/core@1/dist/wasm/hevc-decode.wasm',
});
</script>wasmBinaryUrl is required when assets live on a different origin than the page (Emscripten otherwise fails to resolve the .wasm relative to the worker's blob: URL). Cross-origin worker loading is handled transparently — the plugin fetches the script and wraps it in a same-origin blob URL since classic Workers refuse cross-origin scripts even with CORS headers.
Segment transcoder (manual control)
import { SegmentTranscoder } from '@hevcjs/core';
const transcoder = new SegmentTranscoder({ fps: 25 });
await transcoder.init();
await transcoder.processInitSegment(initSegmentBytes);
const h264Segment = await transcoder.processMediaSegment(hevcSegmentBytes);Compute-aware ABR primitives
@hevcjs/core publishes a SegmentPerfStat on every successful segment transcode. Both @hevcjs/shaka-plugin and @hevcjs/dashjs-plugin consume this bus to cap variants when the device can't keep up. You can subscribe directly if you're integrating with a different player or building custom telemetry:
import { subscribeSegmentStat, ComputeAwareDecider } from '@hevcjs/core';
const detach = subscribeSegmentStat(stat => {
// stat: { totalMs, segDurMs, speedX, frames, width, height }
console.log(`speedX=${stat.speedX} on ${stat.width}x${stat.height}`);
});
// Or use the player-agnostic decider directly
const decider = new ComputeAwareDecider({ measureWindow: 2, lowerAfter: 1 });
decider.setLadderSize(4);
const decision = decider.observe(stat.speedX, currentVariantIdx);
// decision: { capIndex, avgSpeedX, reason: 'init' | 'hold' | 'lower' | 'raise' }How it works
- 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
All heavy work runs in a Web Worker. No SharedArrayBuffer required, no special server headers needed.
Performance
Single-threaded, Apple Silicon:
| Resolution | WASM (Chrome) | |---|---| | 1080p decode | 61 fps | | 4K decode | 21 fps | | 1080p transcode | ~2.5x realtime |
Requirements
- WebAssembly + Web Workers
- WebCodecs (Chrome/Edge 94+, Firefox with H.264 encoding support) for transcoding
- Secure Context (HTTPS or localhost)
License
MIT
