/** * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C). * * Every catalog `fec` object that declares an FEC instance names its block * profile in `fec.blockProfile`. A receiver applies all of a profile's rules * or none of them, so the token is the one explicit per-stream discriminator: * it is never defaulted and never inferred from the wire. * * draft-ramadan-moq-fec §5.1 defines exactly one value, and it is the only * profile this library implements: * * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is * `u16 length || protected packet || zero padding` to T; a block is a * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID; * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2), * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)` * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3). * * A track whose `blockProfile` is absent or names any other token (the * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver * consumes its source without FEC and joins none of its repair tracks * (draft-ramadan-moq-fec §5.1). `parseFecBlockProfile` throws for such a * token; the per-track catalog resolver turns the same rule into a typed * per-track "FEC unavailable" outcome instead of failing the session. */ import { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js' /** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */ export type FecBlockProfile = 'iso-ssbg1-v1' /** Every accepted `fec.blockProfile` token. */ export const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([ 'iso-ssbg1-v1', ]) /** True for a block-profile token this library implements. */ export function isFecBlockProfile(value: unknown): value is FecBlockProfile { return value === 'iso-ssbg1-v1' } /** * Read a required `fec.blockProfile`. Throws on an absent token and on any * token other than `iso-ssbg1-v1`: there is no default and nothing is * inferred from the wire (draft-ramadan-moq-fec §5.1). * * @param where the catalog path of the field, for the error message. */ export function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile { if (isFecBlockProfile(value)) return value if (value === undefined) { throw new Error(`${where} is required: name the FEC block profile ("iso-ssbg1-v1")`) } throw new Error(`${where} must be "iso-ssbg1-v1" (got ${JSON.stringify(value)})`) } /** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */ export const SSBG1_LENGTH_FIELD_BYTES = 2 /** The largest protected packet an ssbg_mode1 symbol of size T can carry. */ export function ssbg1MaxPacketLength(symbolSize: number): number { return symbolSize - SSBG1_LENGTH_FIELD_BYTES } /** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */ export function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean { return ( Number.isSafeInteger(packetLength) && packetLength >= 0 && packetLength <= ssbg1MaxPacketLength(symbolSize) && packetLength <= 0xffff ) } /** * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length * in two octets, network byte order, then the packet, then zero octets up to * T. The protected packet is the MMTP packet before its Source FEC Payload ID * is appended (C.4.2). * * @throws when `length + 2 > T`: the packet cannot be protected under this * profile, and silently truncating it would corrupt every repair symbol. */ export function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array { if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) { throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`) } if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) { throw new Error( `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`, ) } const symbol = new Uint8Array(symbolSize) symbol[0] = (packet.byteLength >>> 8) & 0xff symbol[1] = packet.byteLength & 0xff symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES) return symbol } /** * Recover the protected packet from one ssbg_mode1 source symbol: the bytes * the length field names. Returns null, never a guess, when the length field * points past the symbol or any padding octet is non-zero (C.3.3 padding is * all 00h), since either means the recovered symbol is not what was sent. */ export function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null { if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null const length = (symbol[0]! << 8) | symbol[1]! const end = SSBG1_LENGTH_FIELD_BYTES + length if (end > symbol.byteLength) return null for (let i = end; i < symbol.byteLength; i++) { if (symbol[i] !== 0) return null } return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end) } /** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */ export function isoBlockEnd(ssStart: number, ssbLength: number): number { return serial32Add(ssStart, ssbLength) } /** * Range membership (C.5.2, C.5.3): whether `ssId` is one of the * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32. */ export function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean { return serial32Distance(ssStart, ssId) < ssbLength } /** * A lower bound on how many blocks the block starting at `a` lies behind the * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1 * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least * `n` blocks. Every interleave horizon in the receiver (decoder retention, * worker retirement, ordered-relay settlement) uses this one bound, so the * manager's horizon can never fall inside the decoder's. */ export function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number { if (!Number.isSafeInteger(kMax) || kMax <= 0) { throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`) } return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0 }