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

@blockcast/fec-worker

v0.1.0

Published

Worker-per-track FEC decode with zero-copy ArrayBuffer transfer and ALTA auth plumbing

Downloads

92

Readme

@blockcast/fec-worker

A Web Worker wrapper around @blockcast/mmt-fec that moves the RaptorQ decode off the main thread. The main-thread client posts source and repair symbols to a dedicated worker via postMessage with Transferable ArrayBuffer ownership transfer, so packet payloads cross the thread boundary with zero copies. Decoded / FEC-recovered frames travel back the same way. Critical for IWA / web players where main-thread blocking on FEC decode would cause frame drops at the renderer.

Install

pnpm add @blockcast/fec-worker

The package is dual-shaped: the main-thread side (FecWorkerClient, types) is the default export; the worker entry is exposed as the ./worker subpath so consumers can hand its URL to new Worker(...) (or to the FecWorkerClient constructor, which spawns it for you).

Quick start

import { FecWorkerClient, type FecTrackConfig } from "@blockcast/fec-worker"

// Worker URL resolved against your bundler. ?worker / new URL() / import.meta —
// any pattern that gives you a module-Worker URL works.
const workerUrl = new URL("./fec-worker-entry.js", import.meta.url)

const client = new FecWorkerClient(workerUrl)

client.onFrame((data, meta) => {
  // data is an ArrayBuffer transferred from the worker (zero-copy).
  // meta tells you whether this frame was recovered via FEC and (if ALTA is
  // configured) whether its source-packet trailer verified.
  decoder.decode(data) // hand off to WebCodecs / MSE
})
client.onSnapshot((stats) => fecPanel.render(stats))
client.onError((msg) => console.error("[fec]", msg))

// Configure with a pre-compiled mmt-wasm Module from your loader's importmap.
const config: FecTrackConfig = {
  codec: "avc1.64001f",
  trackName: "video/base",
  trackType: "video",
  resolution: { w: 1920, h: 1080 },
  framerate: 30,
  algorithm: "raptor",
  k: 32,
  repairCount: 8,
  symbolSize: 1312,
  interleaveDepth: 30,
  interleaveMs: 1000, // 30 frames @ 30 fps
  deliveryWindowMs: 2000, // computeFecDeliveryWindowMs(...) from the catalog
  fecMode: "subframe",
  altaEnabled: false,
  relayBlockSigEnabled: false,
}
client.configure(config, mmtWasmModule)

// Feed packets as they arrive from transport. ArrayBuffers are transferred —
// caller MUST NOT touch `data` after the call.
client.feedSource(ssId, packetBuffer, captureTs)
client.feedRepair(ssStart, ssbLength, rsId, repairBuffer, captureTs)

FecWorkerClient accepts either a URL (it spawns the worker for you with { type: "module" }) or a pre-created Worker instance (used in tests for constructor injection).

Why a Worker

A 1080p30 stream with K=32, P=8, symbolSize=1312 produces ~1 RaptorQ source-block decode every second per track. Decode time is amortized across SIMD lanes inside the WASM, but the worst-case block — one with the maximum recoverable loss pattern — can spike past 16 ms. Run that on the main thread and you've blown a frame; the renderer drops, the user sees a stutter, and CMCD reports a pacing violation.

Pushing the decode into a dedicated Worker keeps the main thread free for input handling, MSE buffer maintenance, and frame pacing. The same module compiled into both contexts decodes identically; what changes is who's blocked when a worst-case block hits.

This is the Worker-per-track FEC pipeline architectural decision tracked in the parent monorepo as a v2.0 milestone (pim-multicast-gateway/.planning/MILESTONES.md — "Worker-per-track FEC pipeline with zero-copy ArrayBuffer transfers"). Each track (base / delta / repair / audio) runs in its own Worker, so a stall on one track's decode never starves the others.

Architecture

   Main thread                              │   FEC Worker (per track)
                                            │
   transport packet                         │
        │                                   │
        ▼                                   │
   FecWorkerClient.feedSource(...)          │
        │                                   │
        │  postMessage(cmd, [data, ...])    │
        │   ─ data: ArrayBuffer (transferred, neutered on sender)
        │   ─ wasmModule: WebAssembly.Module (structured-cloned)
        ├──────────────────────────────────►│  self.onmessage
        │                                   │       │
        │                                   │       ▼
        │                                   │  WasmFecDecoder.add_source(...)
        │                                   │       │
        │                                   │       ▼  (recovered or pass-through)
        │                                   │  postMessage({type:"frame", data}, [data])
        │◄──────────────────────────────────┤
        │                                   │
        ▼                                   │
   FecWorkerClient.onFrame(data, meta)      │
        │                                   │
        ▼                                   │
   WebCodecs / MSE                          │

Buffers cross the boundary via the Transferable list. After postMessage, the sender's ArrayBuffer is detached — touching it throws. This is the contract that makes zero-copy safe: there is no aliased view that could be mutated mid-decode.

WebAssembly.Module is structured-cloneable (not transferable) and is cloned (cheap — the underlying compiled module is shared across realms). The ALTA public key, on the other hand, is a regular ArrayBuffer and is transferred.

Errors that escape the worker's try/catch (in either the sync command path or the async configure path) are emitted as {type: "error", message} events rather than thrown — the main thread surfaces them through onError.

Message protocol

All messages are typed as discriminated unions in fec-worker-types.ts. Both the worker and the client import the same definitions, so adding a message variant is a single-file change that the compiler enforces on both sides.

Inbound — FecWorkerCommand (main → worker)

| type | Payload | Transferred | |--------|---------|-------------| | configure | config: FecTrackConfig, wasmModule: WebAssembly.Module, altaPublicKey?: ArrayBuffer, altaWasmModule?: WebAssembly.Module | altaPublicKey only — WASM modules are structured-cloned | | feedSource | ssId: number, data: ArrayBuffer, ts: number, optional altaAuth: ArrayBuffer + altaPayloadEnd: number | data and altaAuth | | feedRepair | ssStart: number, ssbLength: number, rsId: number, data: ArrayBuffer, ts: number | data | | relayBlockSig | sbn: number, sig: ArrayBuffer, hash: ArrayBuffer | sig, hash | | cleanup | sbns: number[] (all timed-out blocks covered by this coalesced barrier) | none | | dispose | — | none |

feedSource carries the flat 32-bit Source Symbol ID from the MMTP Source FEC Payload ID (ISO 23008-1 §C.5.2). The WASM decoder derives SBN = floor(ssId / K) and ESI = ssId % K internally — do not pre-compute these.

feedRepair carries the wire-level Repair FEC Payload ID fields directly (ISO 23008-1 §C.5.3): ssStart is the SS_ID of the first source symbol in the block, ssbLength must equal the track's configured K, and rsId is the repair index within the block. A different K starts a new configured decoder epoch; it is never inferred from repair traffic.

Outbound — FecWorkerEvent (worker → main)

| type | Payload | Transferred back | |--------|---------|------------------| | frame | data: ArrayBuffer, meta: FrameMeta | data | | snapshot | stats: FecTrackStats | none (structured clone) | | blockUpdate | block: FecBlockSnapshot | none | | greenFill | sbn: number, data: ArrayBuffer | data | | cleanupComplete | sbns: number[] | none | | error | message: string | none |

FrameMeta carries recovered: boolean (this frame came out of the FEC decode, not the direct path), sbn: number, and altaVerified: boolean | null (signature outcome, or null when ALTA is not configured / verification is pending).

Lifecycle

new FecWorkerClient(url)
        │  worker spawned with { type: "module" }
        ▼
client.configure(config, wasmModule [, altaPublicKey, altaWasmModule])
        │  worker initSyncs WASM, instantiates MmtFecDecoder(interleaveDepth, videoFill),
        │  immediately configures exact catalog K/T (no decoder defaults),
        │  derives pendingVerifyCap = ceil(D × 1.5) and TTL = ceil(K × interleaveMs × 1.5)
        │  from catalog values, throws if any of K / D / interleaveMs ≤ 0,
        │  starts the 200 ms snapshot timer, emits initial snapshot
        ▼
client.feedSource(...) / feedRepair(...)         ◄── steady state
        │  every transferred ArrayBuffer detaches on the main thread
        │  decoded / recovered frames flow back as {type:"frame"}
        │  every 200 ms a {type:"snapshot"} carries FecTrackStats
        ▼
client.cleanup(sbn)                              ◄── per-block inactivity deadline
        │  coalesces queued SBNs behind one worker queue-drain barrier
        │  evicts blocks relative to max(sbns) − D × 3
        │  emits green-fill with the true SBN from WASM cleanup_blocks_detailed()
        │  acknowledges every SBN in {type:"cleanupComplete", sbns}
        │  any block still in "collecting" at eviction time is counted as failed
        ▼
client.dispose()
           sends {type:"dispose"}, terminates worker. WASM decoder and ALTA
           verifier are explicitly free()'d before terminate so their linear
           memory is released (FinalizationRegistry would not run between
           dispose and terminate). Idempotent.

Backpressure

postMessage is fire-and-forget at the JS level — there is no built-in flow control. If the main thread can't drain frame events fast enough, the worker's outbound queue grows. The remedy is upstream of this package: pace your input (feedSource / feedRepair) against actual decode capacity by watching the avgRecoveryMs / maxRecoveryMs fields of each snapshot. Spikes in maxRecoveryMs past your frame budget are the early signal.

Stats

The snapshot event delivers a FecTrackStats every 200 ms (catalog-derived; not configurable from this layer). Key fields:

| Field | Meaning | |-------|---------| | sourceSymbols, repairSymbols | Raw ingress counters | | blocksComplete | Block had all K source symbols without needing repair | | blocksRecovered | Block was recovered using ≥1 repair symbol | | blocksFailed | Block aged out before reaching K | | greenFillFrames | Frames substituted with green-fill on unrecoverable loss | | recoveryRate | recovered / (recovered + failed), 0–100 | | avgRecoveryMs, maxRecoveryMs | Rolling decode latency (capped at 1000 samples) | | liveSbn | Highest source-block number observed | | blocks | Up to max(D × 3, 24) recent FecBlockSnapshots for live visualization | | interleaveWindowBlocks | D from config |

When ALTA is enabled, the snapshot also carries the four-state verification accounting — altaVerified (crypto passed), altaFailed (tamper-confirmed), altaPending (unverifiable, e.g. missing trailer / late-joiner anchor gap), altaError (verifier threw — infrastructure flake, not tamper) — plus altaInitFailed (true if the ALTA WASM didn't initialize), altaPendingEvictedCap / altaPendingEvictedTtl (deferred-queue eviction reasons), and a bounded altaFailedDetails[] ring with the last ≤16 tamper events. The four-way split exists so ops alerting can distinguish a network problem (altaPending rising) from an attack (altaFailed rising) from a runtime bug (altaError rising).

FecTrackStats is also the shape consumed downstream by @blockcast/mmt-container's CmcdProducer and the diagnostics fec-panel — keep it stable when you extend it.

Related packages

  • @blockcast/mmt-fec — the wrapped library. Container-agnostic RaptorQ decoder + block manager + reorder buffer. Lives on the main thread when you don't need a Worker; this package is the off-thread variant of the same decoder.
  • @blockcast/mmt-container — the consumer that wires this Worker into the data path. FecManager instantiates one FecWorkerClient per track, routes MMTP source/repair payloads in, and pumps recovered frames into the MFU reassembler / fmp4 segmenter.
  • @blockcast/mmt-alta-parse — ALTA trailer extraction. Used inside the Worker to parse the [0xF0A1][len][auth] trailer off recovered source symbols (publisher-stripped trailers don't appear on direct-source packets, which carry the trailer in band).

Testing

pnpm test           # vitest run
pnpm test:watch     # vitest in watch mode
pnpm typecheck      # tsc --noEmit

Tests cover:

  • The message-protocol type narrowing (fec-worker-types.test.ts)
  • The main-thread client's transfer-list construction, callback dispatch, and dispose idempotence (fec-worker-client.test.ts) — the Worker is injected via constructor as a stub so the tests don't need a real WASM build

License

Apache-2.0 — see LICENSE.