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

@safewords/wasm-av1

v0.2.1

Published

AV1 decoding for the browser: rav1d compiled to WebAssembly (baseline, SIMD and multi-threaded builds), with a small streaming API, HLS/CMAF glue and WebGL rendering. A Rust port of GoogleChromeLabs/wasm-av1.

Readme

wasm-av1

AV1 decoding for the browser, in Rust: rav1d (the Rust port of dav1d) compiled to WebAssembly, in baseline, SIMD128 and threads builds (rav1d's frame/tile worker threads as Web Workers on shared memory), behind the small streaming API that GoogleChromeLabs/wasm-av1 put in front of libaom in 2018 — ported from C to Rust here. Containers (MP4, fragmented MP4 / CMAF, WebM/MKV, MPEG-TS) are demuxed inside the same wasm by rivet's rivet-container crate.

Why: a site that serves AV1 wants it to play everywhere. Devices without an AV1 decoder — older phones and laptops, and Safari before AV1 hardware — get a WebAssembly software decoder that the browser JIT compiles to NEON/SSE where the SIMD build is used, and that spreads over the cores where the page allows threads, instead of a fallback rung in another codec.

Nothing upstream is vendored. rav1d and rivet-container are git dependencies (Cargo.lock pins the revisions); upstream wasm-av1 checked all of libaom into third_party/. rav1d comes from safewords/rav1d main (upstream + libc-free build, a panic fix, the wasm SIMD128 kernels, and worker threads on wasm through an embedder spawner — see docs/rav1d.md); rivet-container from rivet develop (docs/rivet.md). pkg/ — the built .wasm + glue — is committed so a git dependency (github:safewords/wasm-av1#sha) carries it; scripts/build.sh regenerates it.

What you get

pkg/baseline/wasm_av1_bg.wasm      post-MVP wasm                     1.70 MB (604 KB gzip)   Chrome 96 / Firefox 89 / Safari 15
pkg/simd/wasm_av1_bg.wasm          + simd128 kernels                 1.79 MB (634 KB gzip)   Chrome 91 / Firefox 89 / Safari 16.4
pkg/threads/wasm_av1_bg.wasm       baseline + atomics/shared memory  1.73 MB                 + cross-origin isolation (Safari 15.2)
pkg/simd-threads/wasm_av1_bg.wasm  simd + atomics/shared memory      1.82 MB                 + cross-origin isolation
pkg/thread-worker.js               the Worker each rav1d thread runs in
js/                                ESM wrapper: loader + feature detection, Decoder, WebGL/2D renderers, Worker, Av1Player, HlsAv1Video

(Without the container feature: ~600 KB less per variant.)

The threads builds run rav1d's own frame + tile threading (dav1d's task pool) with each thread a Web Worker on the same shared memory. They need SharedArrayBuffer, i.e. a cross-origin-isolated page (Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp on the document), and the decoder in a Worker (rav1d blocks its calling thread while workers decode). loadWasmAv1({ threads: true }) picks one when detectThreads() says the page can; Av1Player/HlsAv1Video take { worker: true, threads: 'auto' } and report stats.threads. Measured scaling is in docs/performance.md: 1080p from 16 ms to 3 ms/frame on 8 threads in Chromium, bit-exact.

import { loadWasmAv1, Decoder } from '@safewords/wasm-av1';

const rt  = await loadWasmAv1();          // picks simd or baseline for this browser ({ threads: true } for a threads build, from a Worker)
const dec = new Decoder(rt);
dec.setSource(bytes);                     // IVF, MP4/fMP4, WebM, TS — or setInitSegment()+pushSegment() for CMAF, or pushTemporalUnit(obus, pts)
dec.runUntilFull();                       // decode ahead (ring of 10 frames)
const frame = dec.nextFrame();            // planes in wasm memory: frame.plane(0..2), frame.planes[i].{offset,stride,width,height}
frame.rgba();                             // or RGBA8 (SIMD128 in the SIMD build)

Higher up, Av1Player (canvas, pacing by pts or by an external clock, decode budget per animation frame, in-thread or in a Worker), the renderers — WebGL uploads the planes and does YUV→RGB in the shader; 2D uses the wasm RGBA conversion — and HlsAv1Video: HLS master/media playlists → CMAF init + segments (demuxed by rivet inside the wasm, no decoder reset between segments) → canvas, clocked by a <video> that plays the audio rendition. That last one is the shape a site with an existing HLS player needs — see docs/integration.md for the decision, the pipeline, rung selection, threads/isolation headers and serving. The demo page (npm run serve, then http://localhost:8080/demo/) exercises the file paths.

Consuming it

npm install @safewords/wasm-av1

The package is js/ (the ESM API) + pkg/ (the built .wasm variants and their glue) + docs/. pkg/ must be served as static files, not bundled — copy it into your assets and pass baseUrl to loadWasmAv1/Av1Player/HlsAv1Video. For the Worker, either let your bundler pick up new Worker(new URL('./worker.js', import.meta.url)) or — more predictably — serve js/ statically too and pass workerUrl: '<baseUrl>js/worker.js' (its imports are relative).

Every v* tag is built, tested and published by .github/workflows/release.yml: to npm (with provenance), and on the GitHub Release as public, unauthenticated artefacts for consumers without a registry — safewords-wasm-av1-<ver>.tgz (the same npm pack, installable by URL: "@safewords/wasm-av1": "https://github.com/safewords/wasm-av1/releases/download/v<ver>/safewords-wasm-av1-<ver>.tgz"), wasm-av1-pkg-<ver>.zip (pkg/ alone) and SHA256SUMS. github:safewords/wasm-av1#v<ver> works too (the repo is public and pkg/ is committed).

Building

rustup target add wasm32-unknown-unknown            # rust-toolchain.toml pins 1.96
cargo install wasm-bindgen-cli --version 0.2.127 --locked   # must match Cargo.lock
scoop install binaryen   # or apt/brew — wasm-opt, optional (~15% smaller)
scripts/build.sh                                    # → pkg/{baseline,simd,threads,simd-threads}, pkg/thread-worker.js, pkg/manifest.json

FEATURES=bitdepth_8 scripts/build.sh drops 10/12-bit support for a smaller decoder; FEATURES=bitdepth_8,bitdepth_16 scripts/build.sh drops the container demuxers.

Testing — what runs, and what it proves

| Command | Proves | |---|---| | cargo test --release | Native: IVF parser, geometry, colour conversion against a float reference; every fixture in testdata/ decodes to the same MD5 libdav1d and libaom produce (8-bit, 10-bit, 4:4:4, mono, film grain, odd 177×99); push mode ≡ IVF mode; MP4/fMP4/WebM via rivet ≡ IVF. | | cargo test --release --target wasm32-unknown-unknown --test wasm_convert (± RUSTFLAGS=-C target-feature=+simd128) | Inside wasm, in Node: the RGBA dispatch equals the scalar path on 55 shape/matrix combinations — the SIMD build's v128 code included. | | node --test test/decode.test.mjs (after scripts/build.sh) | The built .wasm, both variants, same MD5s through the wasm-bindgen surface (so the SIMD kernels are bit-exact too); SIMD and baseline RGBA byte-identical; container path; CMAF segments pushed one by one; ms/frame. | | node test/browser.mjs (needs Playwright — PLAYWRIGHT_DIR or a local install) | Headless Chromium + Firefox + WebKit × {baseline, simd, threads, simd-threads} × {WebGL, 2D} × {main thread, Worker} × {IVF, fMP4, MP4, WebM, HLS/CMAF via HlsAv1Video}: every frame shown, correctly paced, pixels on the canvas, the thread count that ran — 54 combinations, on a cross-origin-isolated harness page. | | node scripts/bench-browser.mjs | Decode speed per thread count in the real engines (the threads table above), same frame hash in every configuration. | | scripts/bench.mjs | Decode speed on real 360p/720p/1080p clips (scripts/fetch-samples.sh), with libdav1d MD5 check. |

scripts/make-fixtures.sh regenerates testdata/ with ffmpeg (libaom encode, libdav1d + libaom decode for the reference MD5s — they must agree or it aborts).

Performance (x86-64 laptop)

Single thread, Node 22 / V8 (scripts/bench.mjs):

| clip | baseline decode | simd decode | speed-up | YUV→RGBA baseline → simd | |---|---|---|---|---| | BBB 640×360 | 8.5 ms/frame (117 fps) | 5.9 ms (170 fps) | 1.4× | 0.68 → 0.24 ms (2.8×) | | BBB 1280×720 | 24.0 ms/frame (42 fps) | 9.0 ms (111 fps) | 2.7× | 2.7 → 0.9 ms (2.9×) | | BBB 1920×1080 | 43.2 ms/frame (23 fps) | 16.7 ms (60 fps) | 2.6× | 5.4 → 2.1 ms (2.6×) | | Sintel 1920×818 | 33.1 ms/frame (30 fps) | 18.2 ms (55 fps) | 1.8× | 4.1 → 1.6 ms (2.6×) |

Threads, in the browsers (scripts/bench-browser.mjs, decode inside a Worker, same frame hash in every column):

| clip | engine | simd, 1 thread | 2 threads | 4 threads | 8 threads | |---|---|---|---|---|---| | BBB 1280×720 | Chromium | 8.5 ms/frame | 5.0 ms | 4.0 ms | 3.8 ms (2.2×) | | BBB 1920×1080 | Chromium | 16.0 ms/frame | 8.5 ms | 4.5 ms | 3.0 ms (5.3×) | | BBB 1280×720 | Firefox | 69.5 ms/frame | 41.2 ms | 32.8 ms | 28.2 ms (2.5×) | | BBB 1920×1080 | Firefox | 140.8 ms/frame | 73.9 ms | 40.6 ms | 25.4 ms (5.5×) | | BBB 1280×720 | WebKit | 11.2 ms/frame | 6.7 ms | 5.4 ms | — |

The SIMD build carries wasm SIMD128 kernels for rav1d's hot 8-bit paths — motion compensation (8-tap, warped), CDEF and the deblocking loop filter — written for this project on the safewords/rav1d branch, bit-exact against the scalar code. Firefox's wasm tier is ~4× slower than V8 on the same machine (browser test's decode column). Profile, method and what is left are in docs/performance.md.

Layout

src/ivf.rs        IVF reader (upstream init_avx / blob-api.c)
src/decoder.rs    run/next_frame/finished ring around rav1d (upstream AVX_Decoder_*), IVF / container / push sources
src/frame.rs      packed planes + geometry + colour metadata (upstream buffer_frame)
src/convert.rs    YUV→RGBA: scalar (any layout/depth) + wasm simd128 (8-bit 4:2:0), bit-identical
src/wasm.rs       the wasm-bindgen surface (upstream EXPORTED_FUNCTIONS)
src/threads.rs    the thread spawner for the threads builds (rav1d thread body → Worker)
js/               loader, detect, decoder, render, worker, player, hls, thread-worker
examples/decode_ivf.rs   native CLI (upstream test.c) — MD5, frame dumps, PPM, timings
tests/, test/     native and Node tests;  test/browser.mjs the Playwright matrix (Chromium, Firefox, WebKit)
scripts/          build.sh, bench.mjs (Node), bench-browser.mjs (threads, in the browsers), serve.mjs
docs/             rav1d.md, rivet.md, performance.md, integration.md

Licence: Apache-2.0 (as upstream). See NOTICE for what is linked and under what.