/** * @blockcast/fec-worker — Shared Type Definitions * * Defines the Worker message protocol (commands/events), FecTrackStats shape * consumed by fec-panel (Phase 5) and CmcdProducer (Phase 6), and the relay * block signature stub. * * Types here are importable from both main thread and Worker contexts. */ // --- Config --- /** * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C): the catalog * `fec.blockProfile` token. Required and never inferred; the only profile is * `iso-ssbg1-v1` (C.3.3 ssbg_mode1): SS_ID increments by one per source * packet mod 2^32; a block is `[SS_Start, SS_Start + SSB_length)` keyed by * SS_Start; `k` is only the maximum SSB_length; repair ESI = * SSB_length + RS_ID. The retired `k-padded-v1` token is refused like any * unknown one. * * Mirrors @blockcast/mmt-base `FecBlockProfile`. */ export type FecBlockProfile = "iso-ssbg1-v1"; /** Per-track FEC configuration. Matches design spec section 3.2 FecTrackConfig. */ export interface FecTrackConfig { /** Codec string, e.g. 'avc1.64001f', 'mp4a.40.2' */ codec: string; /** Track name from catalog */ trackName: string; /** Track media type */ trackType: "video" | "audio"; /** Video resolution (omitted for audio) */ resolution?: { w: number; h: number }; /** Video framerate (omitted for audio) */ framerate?: number; /** Audio sample rate (omitted for video) */ sampleRate?: number; /** FEC algorithm identifier: 'raptor', 'reed-solomon', 'none' */ algorithm: string; /** Block profile; the Worker refuses to configure without a known token. */ blockProfile: FecBlockProfile; /** Source symbols per block (K). Under `iso-ssbg1-v1` the maximum SSB_length (kMax). */ k: number; /** Repair symbols per block (P). NOT symbol size. */ repairCount: number; /** Bytes per symbol */ symbolSize: number; /** Retained decoder horizon as a BLOCK COUNT, not D. `isRetiredSbn` * retires blocks more than this many blocks behind the live SS_ID, by * `isoBlocksBehind(SS_ID, live SS_ID, k)` (block-order.ts), the lower * bound every ISO receiver layer shares, and `MmtFecDecoder::new` * documents it as "the number of interleaved blocks". * Never assign `deriveFecInterleaveDepth()` here — that returns media units * per block, which is a different quantity (BLO-35597). */ interleaveDepth: number; /** * Catalog interleave span in integer milliseconds. A positive value is the complete * source-plus-repair span; zero represents an absent/zero catalog span and * carries no elapsed-time deadline. */ interleaveMs: number; /** * Positive cleanup budget. For a positive `interleaveMs` this is the * catalog span plus its explicit reorder allowance. For zero it is a * receiver-local bound only, never an inferred catalog deadline. */ deliveryWindowMs: number; /** FEC encoding mode */ fecMode: "subframe" | "object"; /** MoQ repair track name (optional) */ repairTrack?: string; /** Whether relay block signature verification is enabled (AUTH-01) */ relayBlockSigEnabled: boolean; } // --- Block state --- /** Per-block snapshot for live block visualization and stats. */ export interface FecBlockSnapshot { /** Block key: the block's SS_Start. */ sbn: number; /** Current block state */ state: "collecting" | "complete" | "recovered" | "failed"; /** Number of source symbols received */ sourceReceived: number; /** Number of repair symbols received */ repairReceived: number; /** Configured source-block geometry (K); under `iso-ssbg1-v1` the block's SSB_length. */ k: number; /** Authored source symbols (SSB_length) once a repair symbol declared it. * Under `iso-ssbg1-v1` a block is only known from a repair symbol, so it is * always set — except on a cleanup tombstone for a block the Worker never * saw, whose length nothing declared. */ ssbLength?: number; /** Expected repair symbols (P) */ repairCount: number; /** Timestamp of first symbol arrival (ms) */ firstSymbolTs: number; /** Timestamp of last symbol arrival (ms) */ lastSymbolTs: number; /** Timestamp when block completed (ms), undefined if not yet complete */ completedTs?: number; /** * Cleanup timestamp (ms). It is the catalog completion deadline when * `config.interleaveMs > 0`, and a local retention timestamp otherwise. */ deadlineTs: number; /** Relay block signature validity: true/false after verification, null if not yet verified (AUTH-02) */ relayBlockSigValid: boolean | null; } // --- Stats --- /** Aggregate per-track FEC statistics. Consumed by fec-panel and CmcdProducer. */ export interface FecTrackStats { /** Track configuration */ config: FecTrackConfig; /** Total source symbols received */ sourceSymbols: number; /** Total repair symbols received */ repairSymbols: number; /** Blocks completed without recovery needed */ blocksComplete: number; /** Blocks recovered via FEC */ blocksRecovered: number; /** Blocks that failed (unrecoverable) */ blocksFailed: number; /** Recovery success rate 0-100 */ recoveryRate: number; /** Blocks with valid relay signature */ relayBlocksVerified: number; /** Blocks with invalid relay signature */ relayBlocksFailed: number; /** Live media position: the newest accepted SS_ID / SS_Start. */ liveSbn: number; /** Active block snapshots */ blocks: FecBlockSnapshot[]; /** Number of blocks in the FEC block span */ interleaveWindowBlocks: number; /** Average recovery latency (ms) */ avgRecoveryMs: number; /** Maximum recovery latency (ms) */ maxRecoveryMs: number; /** Repair symbols rejected before WASM because their byte length did not match T. */ repairSymbolSizeMismatch?: number; /** * Source symbols whose SBN had already fallen below the retained window. * * This and its two peers below are dropped before WASM and narrow the * explanations for a block that settles at K-1 source symbols (BLO-35597 * defect 2): a late symbol past retention, a symbol for a block that had * already settled (complete, recovered *or failed*), or a repeated * (SBN, ESI). * * They narrow; they do not uniquely attribute. A symbol lost in transit * never reaches the worker and increments nothing, so all three reading * zero on a short block means only that no *receiver-side* drop was * recorded — wire loss and a short authored block both remain open. Use * `FecBlockSnapshot.ssbLength` to separate them: below `k` is a genuinely * short authored block, equal to `k` with fewer accepted is loss-or-late. * `ssbLength` is undefined until a repair symbol declares it, in which case * both stay open. * * All three are cumulative across decoder epochs: `configure` clears block * tracking but none of them, so they stay comparable against the * `sourceSymbols` denominator. Only a reset zeroes them. */ sourceDroppedRetired?: number; /** Source symbols for a block that had already completed, recovered or failed. */ sourceDroppedTerminal?: number; /** * Source symbols repeating an (SBN, ESI) already accepted in the current * epoch; `configure` clears the accepted-ESI sets. * * Unlike the other two, this one cannot fire once a block settles — the * terminal check returns first, and marking a block terminal retires its * accepted-ESI set anyway — so a non-zero value means the repeat reached * the worker while the block was **still collecting**. It attributes the * timing, not the origin: usually a duplicate packet on the wire, * sometimes an SBN/ESI derivation collision. */ sourceDroppedDuplicate?: number; /** * Accepted source symbols whose block never became known (no repair * symbol named a range containing them) before their SS_ID fell behind * the retained horizon. They were already fed to the decoder and routed * directly; this counts the bookkeeping retired with them, i.e. evidence * that a block's repair symbols never arrived. The decoder retires its * own copies by the same horizon inside `cleanup_by_ss_start`. */ sourceUnplacedRetired: number; /** Native decoder counters, converted to numbers so browser diagnostics remain JSON-safe. */ decoder?: { sourcePackets: number; repairPackets: number; blocksComplete: number; blocksRecovered: number; blocksFailed: number; bytesRecovered: number; pendingBlocks: number; }; } /** Stable identities for Worker failures whose message text contains live values. */ export type FecWorkerErrorCode = | "recovered-symbol-size-unconfigured" | "recovered-block-too-short" | "recovered-symbol-ssb-length-invalid" | "repair-symbol-size-mismatch" | "repair-ssb-length-conflict" /** `iso-ssbg1-v1`: a repair's `[SS_Start, SS_Start + SSB_length)` overlaps a different known block. */ | "repair-block-overlap" /** `iso-ssbg1-v1`: a recovered symbol's ssbg_mode1 length field exceeds T-2 or its padding is non-zero. */ | "recovered-ssbg1-symbol-invalid" /** `iso-ssbg1-v1`: the decoder returned a recovered block for a source the Worker could not place in a known block. */ | "recovered-block-unplaced" | "reset-failed" | "unknown-command" | "command-failed" | "worker-runtime" | "worker-message"; // --- Frame metadata --- /** Per-frame metadata emitted with frame events from Worker. */ export interface FrameMeta { /** Whether this frame was recovered via FEC (not received directly) */ recovered: boolean; /** Block key this frame belongs to: the block's SS_Start */ sbn: number; /** Encoding Symbol ID within the recovered source block (0..ssbLength-1). * Under `iso-ssbg1-v1` a recovered frame is the full T-byte ssbg_mode1 * symbol (length field, packet, zero padding); the Worker validates but * never trims it. */ esi?: number; /** * Authored source-symbol count of the recovered block (the repair * symbols' SSB_length, 1..K). Recovered frames are ESI 0..ssbLength-1; * ESIs ssbLength..K-1 are encoder zero padding and are never emitted. */ ssbLength?: number; } // --- Worker commands (main -> worker) --- /** Discriminated union of all commands sent from main thread to Worker. */ export type FecWorkerCommand = | { type: "configure"; config: FecTrackConfig; wasmModule: WebAssembly.Module; } | { type: "feedSource"; /** Flat 32-bit Source Symbol ID from the MMTP Source FEC Payload ID * (ISO 23008-1 §C.5.2). SS_IDs are consecutive per source packet mod * 2^32 and the block comes from a repair's range — do NOT * pre-compute a block. */ ssId: number; /** The protected packet exactly, unpadded and without its SS_ID (at * most T-2 bytes). */ data: ArrayBuffer; ts: number; } | { type: "feedRepair"; /** SS_Start: SS_ID of the first source symbol in the block, from the * MMTP Repair FEC Payload ID (ISO 23008-1 §C.5.3): any uint32; the * block key. */ ssStart: number; /** Authored source-symbol count for this block; 1..configured K. */ ssbLength: number; /** RS_ID: repair symbol index within the block; WASM maps to repair * ESI = SSB_length + RS_ID. */ rsId: number; data: ArrayBuffer; ts: number; } | { /** * Every source and repair symbol the page fed in one task, in feed * order, sharing one transfer list. The client sends symbols this * way instead of one message per symbol (BLO-36599); the Worker * handles each record exactly like the single-symbol command of the * same `type`, and a record that fails does not stop the others. */ type: "feedSymbols"; symbols: FecSymbolCommand[]; } | { type: "relayBlockSig"; sbn: number; sig: ArrayBuffer; hash: ArrayBuffer; } | { type: "cleanup"; /** Every timed-out block covered by this coalesced queue-drain barrier, * by block key (SS_Start). */ sbns: number[]; } | { type: "reset" } /** * Ask the Worker to post a blockUpdate on every block transition. Without * it the Worker posts none: each one is a structured clone and a task on * the page thread, and the player does not consume them (BLO-36599). * Sent only to a Worker that announced it in `capabilities`. */ | { type: "subscribeBlockUpdates" } | { type: "dispose" }; /** One symbol record of a `feedSymbols` batch. */ export type FecSymbolCommand = Extract< FecWorkerCommand, { type: "feedSource" } | { type: "feedRepair" } >; // --- Worker events (worker -> main) --- /** Discriminated union of all events emitted from Worker to main thread. */ export type FecWorkerEvent = | { type: "frame"; data: ArrayBuffer; meta: FrameMeta } | { type: "snapshot"; stats: FecTrackStats } | { type: "blockUpdate"; block: FecBlockSnapshot } | { type: "cleanupComplete"; sbns: number[] } | { type: "resetComplete" } /** * Optional commands this Worker accepts, posted once it is configured. * The client uses `feedSymbols` only after the Worker names it, so a page * that meets an older cached Worker bundle keeps sending one command per * symbol instead of a batch that Worker would reject (BLO-36599). */ | { type: "capabilities"; commands: FecWorkerOptionalCommand[] } | { type: "error"; message: string; code?: FecWorkerErrorCode }; /** Commands a client may send only after the Worker announced them. */ export type FecWorkerOptionalCommand = "feedSymbols" | "subscribeBlockUpdates";