npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ferrite.js

v1.3.1

Published

Canvas + WASM software/WebCodecs video player — a drop-in for mpegts.js for the no-hardware-HEVC niche.

Readme

ferrite.js

A canvas + WebAssembly video player for the browser, shaped as a drop-in replacement for mpegts.js and geared for IPTV — live MPEG-TS channels and VOD movies/series (MP4/MKV) as delivered by IPTV providers (Xtream / M3U / Stalker). It decodes with a software FFmpeg-WASM path and a hardware WebCodecs path, rendering to a <canvas> with WebAudio — so it plays codecs the browser's <video> element can't, most notably HEVC where there is no hardware decoder.

URL ─▶ fetch ─▶ [worker] demux (mpegts/PES) ─▶ decode ─┬─ software: FFmpeg-WASM ─▶ YUV planes ─┐
                                                        └─ hardware: WebCodecs   ─▶ VideoFrame ─┴─▶ WebGL2 <canvas>
                                            audio: FFmpeg-WASM ─▶ PCM ─▶ WebAudio (master clock)

Why this exists — the no-HW-HEVC niche

<video> + mpegts.js/MSE can only play what the browser's media stack supports. On a machine with no hardware HEVC decoder (and increasingly that is the default in browsers/PWAs), HEVC simply won't play — addSourceBuffer('video/mp4; codecs="hvc1"') is rejected. ferrite.js carries its own FFmpeg decoder compiled to WebAssembly and decodes HEVC in software, on worker threads, fast enough for live 4K. Where the browser does have a hardware decoder for a codec (e.g. H.264), it uses WebCodecs instead for near-zero CPU. That two-tier behaviour is automatic and per-stream.

This is a genuine gap mpegts.js cannot fill (it is bound to MSE/<video>), which is the whole reason ferrite.js diverges from a literal drop-in in exactly one place (see below).

Two decode tiers

| Tier | Path | When | |---|---|---| | software | FFmpeg → WASM (pthreads) → YUV → WebGL2 | always available; the only HEVC path on a no-HW box; interlaced / MPEG-2 | | webcodecs | VideoDecoderVideoFrame → WebGL2 | when the browser hardware-decodes the stream's exact codec (e.g. H.264) |

Tier selection happens once the demuxer reveals the codec; it falls back to software cleanly (an unsupported codec, or a HW reject before the first frame, never produces a dead screen). The active tier is reported on player.tier / statisticsInfo.tier.

Supported formats

Geared for IPTV delivery — live MPEG-TS channels and VOD (movies/series) streamed over HTTP, in the common Xtream/M3U/Stalker shapes. The bundled engine is a scoped FFmpeg subset:

| | | |---|---| | Containers | MPEG-TS, MP4 / MOV, Matroska / WebM — probed by content, not the URL extension (IPTV .mp4 URLs are frequently MKV) | | Video | H.264 / AVC · H.265 / HEVC · MPEG-2 — 8- and 10-bit, with software deinterlacing (bwdif) | | Audio | AAC · AC-3 · E-AC-3 (Dolby Digital / Digital Plus) · MP2 · MP3 |

Audio is always decoded in software (it is the master clock that paces video); video uses the hardware WebCodecs tier when the browser can decode the stream's exact codec, otherwise software. Codecs outside this set surface a clean unsupported-codec error rather than a dead screen.

Install

npm install ferrite.js

The package ships the prebuilt engine in assets/ferrite.{mjs,wasm} (~2 MB wasm). Copy those two files to a path your app serves and point the player at it with wasmBaseUrl (they must be served same-origin under COOP/COEP — see below). For example, with Vite, copy them into public/assets/ and set wasmBaseUrl: '/assets/'.

Usage — the mpegts.js shape

import Ferrite, { Events } from 'ferrite.js';

const player = Ferrite.createPlayer(
  { type: 'mpegts', isLive: true, url: 'https://…/stream.ts' },
  { wasmBaseUrl: '/assets/', liveSync: true },
);

player.attachCanvas(document.querySelector('canvas'));   // ← the one divergence (see below)
player.on(Events.ERROR, (type, details, info) => console.error(type, details, info));
player.load();
await player.play();

// teardown (same call sequence as mpegts.js):
player.pause();
player.unload();
player.detachMediaElement();
player.destroy();

The static namespace mirrors mpegts.js: createPlayer, isSupported, getFeatureList, version, Events, ErrorTypes, ErrorDetails, LoaderErrors. The player methods mpegts.js consumers expect — load / play / pause / unload / detachMediaElement / destroy / on / off — are all present, and on(Events.ERROR, …) fires (type, details, info) with the verbatim mpegts.js ErrorTypes / ErrorDetails strings, so an existing mpegts.js error classifier works unchanged.

The one divergence: attachCanvas instead of attachMediaElement

ferrite.js owns a <canvas> + WebAudio, not a <video>/MSE pipeline, so it cannot attach to a <video> element. Use player.attachCanvas(canvas). attachMediaElement() is kept as a guard that throws loudly if you wire it like mpegts.js by habit. play / pause / currentTime / volume / muted live on the player (there is no <video> to delegate to).

Optional built-in controls + debug overlay

Because there is no <video>, there are no native controls. ferrite.js ships an opt-in, framework-free control bar + diagnostic overlay you can attach in one call:

import { attachControls } from 'ferrite.js/controls';

const controls = attachControls(player, canvas);   // auto-hiding bar + long-press debug overlay
// ... later
controls.destroy();
  • Controls bar (auto-hides after idle): play/pause, mute, volume, fullscreen, a LIVE badge.
  • Debug overlaylong-press the video to toggle (off by default). Rows: isolated, tier, format, status, clock. It is the perfect first-run diagnostic on a device with no devtools (e.g. an iPad PWA): if it shows isolated: NO (no SharedArrayBuffer), your page is not cross-origin isolated and the decoder cannot run — fix COOP/COEP (below).

attachControls(player, target, options?) accepts either the <canvas> (it wraps it in a positioned shell) or a container that already holds the canvas. Options: autoHide, idleHideMs, longPressMs, debugOverlay, persistVolume, volumeStorageKey.

⚠ Requirement: a cross-origin-isolated secure context (COOP/COEP)

ferrite.js uses SharedArrayBuffer (worker decode threads + the WebCodecs path), which the browser only exposes when the page is cross-origin isolated AND in a secure context (HTTPS or localhost). You must serve the page with:

Cross-Origin-Opener-Policy:   same-origin
Cross-Origin-Embedder-Policy:  require-corp        # or: credentialless

and the engine assets same-origin with Cross-Origin-Resource-Policy: same-origin. Without isolation, crossOriginIsolated === false, there is no SharedArrayBuffer, and load() surfaces an explicit unsupported-codec error instead of dying silently. The debug overlay's isolated: NO row is the fastest way to confirm this in the field.

Notes:

  • COEP: require-corp blocks cross-origin subresources without CORP headers (e.g. channel logos from arbitrary providers). Use COEP: credentialless (Chromium) or proxy those subresources same-origin if that bites.
  • Plain HTTP on a LAN IP is not a secure context — crossOriginIsolated will be false even with the headers set. Use HTTPS or localhost (e.g. an SSH tunnel).
  • The video stream itself should be fetched same-origin (proxy cross-origin streams), since COEP applies to the worker's fetch.

Configuration highlights

createPlayer(dataSource, config?) — key config fields (see FerriteConfig for the full set):

| field | default | meaning | |---|---|---| | wasmBaseUrl | '/' | base URL serving ferrite.mjs + ferrite.wasm | | threads | 8 | software decoder pthread pool size | | preferWebCodecs | true | use the hardware tier when the codec is supported | | liveSync | false | enable live latency-sync via playback-rate (set true for live) | | liveSyncTargetLatency | 0.6 | target latency (s) the player converges toward | | liveSyncPlaybackRate | 1.05 | max catch-up rate (sub-audible pitch; not mpegts's 1.2) | | stashAdaptive / stashInitialSize / stashMaxSize | true / floor / 2 MiB | adaptive pre-demux buffer (low latency on SD/HD, full-PES floor for 4K) | | workerUrl | (auto) | override the decode-worker URL (see "Bundlers" below) |

Bundlers

The decode worker is spawned with the literal new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) form, which webpack, Vite, and esbuild detect and bundle automatically — so the zero-config path works for most setups. If your toolchain can't trace it (an unusual asset pipeline, a strict CSP, or a bundler that doesn't support module workers), copy dist/worker.js to a path you serve and pass config.workerUrl pointing at it.

Live tuning (adaptive low-water + sigmoid playback-rate latency-sync with a dead-band/gate to avoid hunting) is derived from a battle-tested reference player; the defaults are good for IPTV.

Demo

npm install
npm run build      # tsup → dist/ (ESM + .d.ts)
npm run demo       # → http://localhost:8650/

The demo server sets COOP/COEP/CORP and exposes /proxy?url=<stream> to pull a cross-origin stream same-origin. Open the page (the header shows ● isolated), paste an MPEG-TS URL (prefix cross-origin URLs with /proxy?url=), and press Play. Hover for controls; long-press the video for the debug overlay.

Building from source / rebuilding the engine

The TypeScript player builds with tsup:

npm run build       # dist/index.js, dist/worker.js, dist/controls.js + .d.ts
npm run typecheck   # tsc --noEmit

The WebAssembly engine (assets/ferrite.{mjs,wasm}) is shipped prebuilt so npm consumers never have to build it. Its C source + self-contained build script live in this repo under engine/, so you can rebuild it standalone (needs emsdk):

cd engine && bash build-engine.sh         # → assets/ferrite.{mjs,wasm} (matched pair, in place)

The build downloads a pristine FFmpeg (version pinned in engine/ffmpeg-version), cross-compiles the decode subset to wasm, links ferrite.c, and writes the matched .mjs+.wasm pair directly into assets/. The engine is a threaded FFmpeg subset (mpegts/matroska/mov demux; HEVC/H.264/MPEG-2 video + AAC/AC-3/E-AC-3/MP2/MP3 audio; bwdif deinterlacer), -pthread + SIMD, growable shared memory (256 MiB → 2 GiB), exporting HEAPU8 + PThread. See engine/README.md for the full build details, and tests/README.md for the node verification gates.

API surface

  • createPlayer(dataSource, config?) → FerritePlayer
  • isSupported() → boolean, getFeatureList() → Record<string, boolean> (incl. crossOriginIsolated)
  • EventsERROR, MEDIA_INFO, STATISTICS_INFO, LOADING_COMPLETE, RECOVERED_EARLY_EOF, DESTROYING, plus ferrite extensions (TIME_UPDATE, LOG, DEINT_FAILED)
  • ErrorTypes / ErrorDetails / LoaderErrors — verbatim mpegts.js strings
  • FerritePlayerattachCanvas, load, play, pause, unload, detachMediaElement, destroy, recover, on/off; props paused, currentTime, duration, volume, muted, tier, videoWidth/videoHeight, mediaInfo, statisticsInfo
  • ferrite.js/controlsattachControls(player, target, options?) → { destroy() }

License

MIT for the player (everything under src/, the engine glue, build scripts, tests, demo, and the generated assets/ferrite.mjs loader). The vendored engine binary assets/ferrite.wasm is a derivative of FFmpeg and is licensed LGPL-2.1-or-later — it is built from a purpose-built FFmpeg 8.1.1 decode subset configured without --enable-gpl/--enable-nonfree (no GPL or non-free components). The full LGPL text is in engine/COPYING.LGPLv2.1, and engine/ ships the complete source + build script so the engine can be rebuilt/relinked against a modified FFmpeg (LGPL §6). See LICENSE for the full terms.

Patent note: the bundled decoders (HEVC/H.264/MPEG-2, AAC/AC-3/E-AC-3/MP2/MP3) implement formats that may be patent-encumbered in some jurisdictions. That is separate from the copyright license and is the redistributor's responsibility to evaluate.