/** * Catalog FEC source rules shared by every catalog reader (BLO-40851). * * These are the dialect-level reads the container resolvers * (`resolveRepairLayers`, `catalogFecUnavailable`), the transport endpoint * selection (`selectEndpointPacketIds`) and a player's repair inventory all * need to agree on. They live here, in the leaf package every one of those * depends on, so there is one copy of each rule instead of copies pinned * against each other by tests. * * - `locateCatalogFecSource`: which catalog entry declares a source's `fec` * block. hang `video`/`audio` rendition blocks are authoritative over an MSF * `tracks[]` entry of the same name. * - `catalogFecAvailable`: draft-ramadan-moq-fec §5.1, whether a block's FEC * is usable at all (implemented `blockProfile`, usable span). * - `catalogFecDeclaresUnconfigurableInstance`: a declared FEC instance whose * geometry a decoder cannot configure, which is a malformed catalog rather * than "no FEC". * * Every function is total: it never throws, whatever the input. */ import { isFecBlockProfile } from './fec-block-profile.js' /** The catalog media roles that carry hang renditions. */ export type CatalogFecRole = 'video' | 'audio' /** One media source's `fec` block, as `locateCatalogFecSource` found it. */ export type CatalogFecSource = | { readonly dialect: 'hang' readonly role: CatalogFecRole /** The rendition object. */ readonly entry: Record /** The rendition's `fec` value, unvalidated (may be undefined). */ readonly fec: unknown /** JSON path of that `fec` block. */ readonly fecPath: string } | { readonly dialect: 'msf' /** Index of the entry in `tracks[]`. */ readonly index: number /** The `tracks[]` entry (an empty object when the entry is not an object). */ readonly entry: Record /** The entry's `fec` value, unvalidated (may be undefined). */ readonly fec: unknown /** JSON path of that `fec` block. */ readonly fecPath: string } const ALL_ROLES: readonly CatalogFecRole[] = Object.freeze(['video', 'audio']) function recordOf(value: unknown): Record | undefined { return value !== null && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : undefined } /** * Locate the catalog entry that declares one media source's `fec` block, in * either catalog dialect. * * A hang `video`/`audio` rendition named `source` wins, in `roles` order, over * a `tracks[]` entry of the same name: when a catalog carries rendition blocks * beside a `tracks[]` switching-set index (the unified ABR ladder catalog), * the rendition blocks are authoritative, exactly as they are for hang's own * catalog decode. The precedence is keyed on the rendition EXISTING as an * object, not on it carrying `fec`: a rendition without `fec` locates with * `fec: undefined` and never falls through to `tracks[]`. * * Otherwise the first `tracks[]` entry named `source` locates. Returns * undefined when neither dialect names it, or when `catalog` is not an object. * * @param roles the hang roles to search, in order (both by default). A typed * track reference passes its own role only. */ export function locateCatalogFecSource( catalog: unknown, source: string, roles: readonly CatalogFecRole[] = ALL_ROLES, ): CatalogFecSource | undefined { const root = recordOf(catalog) if (root === undefined) return undefined for (const role of roles) { const renditions = recordOf(recordOf(root[role])?.['renditions']) if (renditions === undefined || !Object.hasOwn(renditions, source)) continue const entry = recordOf(renditions[source]) if (entry === undefined) continue return Object.freeze({ dialect: 'hang' as const, role, entry, fec: entry['fec'], fecPath: `$.${role}.renditions[${JSON.stringify(source)}].fec`, }) } const tracks = root['tracks'] if (!Array.isArray(tracks)) return undefined const index = tracks.findIndex((track) => recordOf(track)?.['name'] === source) if (index < 0) return undefined const entry = recordOf(tracks[index]) ?? {} return Object.freeze({ dialect: 'msf' as const, index, entry, fec: entry['fec'], fecPath: `$.tracks[${index}].fec`, }) } /** * Whether an `iso-ssbg1-v1` block's optional `interleaveDepthMs` span is * usable: absent, or a non-negative safe integer. Zero or absence supplies no * elapsed deadline and still permits in-band FEC. True for every block that * names another profile (the span is not read) and for a non-object. */ export function isoBlockSpanUsable(fec: unknown): boolean { const block = recordOf(fec) if (block === undefined || block['blockProfile'] !== 'iso-ssbg1-v1') return true const ms = block['interleaveDepthMs'] return ms === undefined || (typeof ms === 'number' && Number.isSafeInteger(ms) && ms >= 0) } /** * Whether one catalog `fec` block leaves its FEC available * (draft-ramadan-moq-fec §5.1): the decision the container's * `catalogFecUnavailable` explains with a typed reason. * * False when the block declares an FEC instance (any `algorithm` but `"none"`) * and names no implemented `blockProfile` (the retired `k-padded-v1` * included), or carries an `iso-ssbg1-v1` span it cannot use; and when a block * declaring no FEC instance carries a profile token that is not implemented. * True for a non-object and for every other block. It reads only the profile * and the span: malformed geometry (`sourceSymbols`, `symbolSize`) is * `catalogFecDeclaresUnconfigurableInstance`'s. * * A receiver applies no FEC to a source whose block is unavailable and joins * none of its repair tracks, without failing the session. */ export function catalogFecAvailable(fec: unknown): boolean { const block = recordOf(fec) if (block === undefined) return true const declaresFecInstance = block['algorithm'] !== 'none' const profile = block['blockProfile'] const profileOk = isFecBlockProfile(profile) || (profile === undefined && !declaresFecInstance) return profileOk && (!declaresFecInstance || isoBlockSpanUsable(block)) } function isPositiveSafeInteger(value: unknown): boolean { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 } /** * Whether a source's `fec` block declares an FEC instance (any `algorithm` * but `"none"`) whose `sourceSymbols` or `symbolSize` is not a positive * integer. Geometry a decoder cannot configure is a malformed catalog, not * "no FEC": the container's per-track resolver throws `track_fec` for exactly * this block, whatever its profile or span. */ export function catalogFecDeclaresUnconfigurableInstance(fec: unknown): boolean { const block = recordOf(fec) if (block === undefined || block['algorithm'] === 'none') return false return !(isPositiveSafeInteger(block['sourceSymbols']) && isPositiveSafeInteger(block['symbolSize'])) }