# @svta/cml-608 CTA-608/CEA-608 closed captioning functionality. ## When to use - Extract CTA-608 caption bytes from an AVC or HEVC sample: `extractCta608DataFromSample`, `findCta608Nalus` - Extract CTA-608 caption bytes from an AV1 sample: `extractCta608DataFromAv1Sample` - Decode caption byte pairs into timed caption screens and cues: `Cta608Parser`, `CaptionScreen` - Parse a Scenarist Closed Caption file: `SccParser` Do not write your own SEI or OBU scanner, 608 command decoder, or caption screen model. Open an issue for a command or layout that is missing. ## Installation ```bash npm i @svta/cml-608 ``` ## Usage ```typescript import { CaptionScreen, Row } from "@svta/cml-608"; ``` ### Extracting captions from a media sample CTA-608 captions are embedded in the video elementary stream as an ATSC A/53 `cc_data()` payload. Only the envelope around the payload differs by codec. The envelope is either a SEI message in a NAL unit, or an OBU. Choose the extractor that matches the sample entry type: | Sample entry | Envelope | Extractor | | ----------------------- | --------------------------------- | ---------------------------------- | | `avc1` / `hvc1` / `vvc1` | `itu_t_t35` SEI message in a NAL | `extractCta608DataFromSample` | | `av01` | `metadata_itu_t_t35` OBU | `extractCta608DataFromAv1Sample` | Both extractors return the same `[field1, field2]` byte arrays. `Cta608Parser` renders them as caption screens: ```typescript import { Cta608Parser, extractCta608DataFromAv1Sample } from "@svta/cml-608"; import type { CaptionScreen } from "@svta/cml-608"; const parser = new Cta608Parser(1, { newCue(startTime: number, endTime: number, screen: CaptionScreen) { console.log(startTime, endTime, screen.getDisplayText()); }, }, null); // Call once per AV1 sample. The sample starts at samplePos in view. function onSample(view: DataView, samplePos: number, sampleSize: number, sampleTimeMs: number): void { const [field1] = extractCta608DataFromAv1Sample(view, samplePos, sampleSize); if (field1.length) { parser.addData(sampleTimeMs, field1); } } ``` The two extractors are separate because the two envelopes are framed differently, and a sample does not announce its framing. A NAL unit sample puts a fixed-width big-endian length before each unit. Only `lengthSizeMinusOne` in the `avcC` or `hvcC` config gives that width. An AV1 sample uses the low-overhead format. Each OBU has its own `obu_size` after the header, and the last OBU may omit it. In both cases the sample entry gives the framing. ## API Report File for "@svta/cml-608" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ValueOf } from '@svta/cml-utils'; // @public export type CaptionModes = "MODE_ROLL-UP" | "MODE_POP-ON" | "MODE_PAINT-ON" | "MODE_TEXT" | null; // @public export class CaptionScreen { constructor(logger?: CaptionsLogger); // (undocumented) backSpace(): void; // (undocumented) clearToEndOfRow(): void; // (undocumented) copy(other: CaptionScreen): void; // (undocumented) equals(other: CaptionScreen): boolean; getDisplayText(asOneRow?: boolean): string; // (undocumented) getTextAndFormat(): Row[]; insertChar(char: number): void; // (undocumented) isEmpty(): boolean; // (undocumented) moveCursor(relPos: number): void; // (undocumented) reset(): void; // (undocumented) rollUp(): void; // (undocumented) rows: Row[]; setBkgData(bkgData: Partial): void; // (undocumented) setCursor(absPos: number): void; // (undocumented) setPAC(pacData: PACData): void; // (undocumented) setPen(styles: Partial): void; // (undocumented) setRollUpRows(nrRows: number | null): void; } // @public export class CaptionsLogger { // (undocumented) log(severity: VerboseLevel, msg: string | (() => string)): void; // (undocumented) time: number | null; // (undocumented) verboseLevel: VerboseLevel; } // @public export class Cta608Channel { constructor(channelNumber: number, outputFilter: CueHandler, logger?: CaptionsLogger); // (undocumented) ccAOF(): void; // (undocumented) ccAON(): void; // (undocumented) ccBS(): void; // (undocumented) ccCR(): void; // (undocumented) ccDER(): void; // (undocumented) ccEDM(): void; // (undocumented) ccENM(): void; // (undocumented) ccEOC(): void; // (undocumented) ccFON(): void; // (undocumented) ccMIDROW(secondByte: number): void; // (undocumented) ccRCL(): void; // (undocumented) ccRDC(): void; // (undocumented) ccRTD(): void; // (undocumented) ccRU(nrRows: number | null): void; // (undocumented) ccTO(nrCols: number): void; // (undocumented) ccTR(): void; // (undocumented) chNr: number; // (undocumented) cueSplitAtTime(t: number): void; // (undocumented) cueStartTime: number | null; // (undocumented) currRollUpRow: Row; // (undocumented) displayedMemory: CaptionScreen; // (undocumented) getHandler(): CueHandler; // (undocumented) insertChars(chars: number[]): void; // (undocumented) lastOutputScreen: CaptionScreen; // (undocumented) mode: CaptionModes; // (undocumented) nonDisplayedMemory: CaptionScreen; // (undocumented) outputDataUpdate(dispatch?: boolean): void; // (undocumented) outputFilter: CueHandler; // (undocumented) reset(): void; // (undocumented) setBkgData(bkgData: Partial): void; // (undocumented) setHandler(outputFilter: CueHandler): void; // (undocumented) setMode(newMode: CaptionModes): void; // (undocumented) setPAC(pacData: PACData): void; // (undocumented) writeScreen: CaptionScreen; } // @public export class Cta608Parser { constructor(field: SupportedField, out1: any, out2: any); addData(time: number | null, byteList: number[]): void; cueSplitAtTime(t: number): void; reset(): void; } // @public export type CueHandler = { newCue(startTime: number, endTime: number, screen: CaptionScreen): void; reset?(): void; dispatchCue?(): void; }; // @public export function extractCta608Data(raw: DataView, cta608Range: number[]): number[][]; // @public export function extractCta608DataFromAv1Sample(raw: DataView, startPos: number, sampleSize: number): number[][]; // @public export function extractCta608DataFromSample(raw: DataView, startPos: number, sampleSize: number): number[][]; // @public export function findCta608Nalus(raw: DataView, startPos: number, size: number): number[][]; // @public export type PACData = { row: number; indent: number | null; color: string | null; underline: boolean; italics: boolean; }; // @public export class PenState { // (undocumented) background: string; // (undocumented) copy(newPenState: PenState): void; // (undocumented) equals(other: PenState): boolean; // (undocumented) flash: boolean; // (undocumented) foreground: string | null; // (undocumented) isDefault(): boolean; // (undocumented) italics: boolean; // (undocumented) reset(): void; // (undocumented) setStyles(styles: Partial): void; // (undocumented) underline: boolean; } // @public export type PenStyles = { foreground: string | null; underline: boolean; italics: boolean; background: string; flash: boolean; }; // @public export class Row { constructor(logger?: CaptionsLogger); backSpace(): void; // (undocumented) chars: StyledUnicodeChar[]; // (undocumented) clear(): void; // (undocumented) clearFromPos(startPos: number): void; // (undocumented) clearToEndOfRow(): void; // (undocumented) copy(other: Row): void; // (undocumented) cueStartTime: number | null; // (undocumented) equals(other: Row): boolean; // (undocumented) getTextString(): string; // (undocumented) insertChar(byte: number): void; // (undocumented) isEmpty(): boolean; moveCursor(relPos: number): void; setCursor(absPos: number): void; // (undocumented) setPenStyles(styles: Partial): void; } // @public export class SccParser { constructor(processor: Pick, field?: number); // (undocumented) field: number; // (undocumented) getField(): number; // (undocumented) getHeaderStatus(): boolean; // (undocumented) getLinesParsed(): number; // (undocumented) hasHeader: boolean; // (undocumented) nrLinesParsed: number; // (undocumented) parse(text: string): void; // (undocumented) parseDataLine(line: string): [number, number[]] | null; // (undocumented) processor: Pick; // (undocumented) timeConverter(smpteTs: string): number; } // @public export class StyledUnicodeChar { // (undocumented) copy(newChar: StyledUnicodeChar): void; // (undocumented) equals(other: StyledUnicodeChar): boolean; // (undocumented) isEmpty(): boolean; // (undocumented) penState: PenState; // (undocumented) reset(): void; // (undocumented) setChar(uchar: string, newPenState: PenState): void; // (undocumented) setPenState(newPenState: PenState): void; // (undocumented) uchar: string; } // @public export type SupportedField = 1 | 3; // @public export const VerboseLevel: { readonly ERROR: 0; readonly TEXT: 1; readonly WARNING: 2; readonly INFO: 2; readonly DEBUG: 3; readonly DATA: 3; }; // @public (undocumented) export type VerboseLevel = ValueOf; // (No @packageDocumentation comment for this package) ``` # @svta/cml-abr Bitrate selection functionality. The package defines `BitrateSelector`, the type that every bitrate selection algorithm implements. A selector takes a bitrate ladder, the buffer level, and a throughput estimate. It returns one index into the ladder. The package ships two selectors. `createThroughputSelector` creates a rate-based selector that picks the highest rung that the throughput estimate sustains. `createBolaSelector` creates a buffer-based selector that implements BOLA. The package implements the [bitrate selector RFC](https://github.com/streaming-video-technology-alliance/common-media-library/pull/526) and the [BOLA selector RFC](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/rfc/bola-abr-selector/index.md). Every export is marked `@beta`. ## When to use - Pick a bitrate ladder rung from a throughput estimate and the buffer level: `createThroughputSelector` - Pick a rung from the buffer level with BOLA, and tune its safety factors: `createBolaSelector`, `BolaSelectorOptions` - Implement a custom bitrate selection algorithm behind the shared interface: `BitrateSelector`, `BitrateSelectorInput`, `BitrateDecision` - Pass segment download samples to a selector: `SegmentSample` - Tune the safety factor, the upgrade factor, and the starvation tolerance: `ThroughputSelectorOptions`, `DEFAULT_SAFETY_FACTOR` Do not rewrite the rate-based or BOLA selectors. For a different algorithm, implement `BitrateSelector`. Open an issue for a selector type that is missing. ## Installation ```bash npm i @svta/cml-abr ``` ## Usage ```typescript import { createThroughputSelector } from "@svta/cml-abr"; const selector = createThroughputSelector(); const ladder = [{ bandwidth: 300_000 }, { bandwidth: 750_000 }, { bandwidth: 1_500_000 }, { bandwidth: 3_000_000 }, { bandwidth: 6_000_000 }]; const { index } = selector.select({ ladder, bufferLevel: 4, throughput: 3_000_000, now: 0 }); console.log(index); // 2. The highest rung at or below 0.9 * 3 Mbps is 1.5 Mbps. ``` ## Player integration A player creates one estimator and one selector for each media type. A player with one shared estimator passes the same estimate to both selectors. Before every segment request, the player calls `select`. After every finished download, the player calls `sample` on the estimator and on the selector with the same timing object. ```typescript import { createThroughputSelector } from "@svta/cml-abr"; import { EwmaEstimator } from "@svta/cml-throughput"; const ladder = [{ bandwidth: 300_000 }, { bandwidth: 750_000 }, { bandwidth: 1_500_000 }, { bandwidth: 3_000_000 }, { bandwidth: 6_000_000 }]; const estimator = new EwmaEstimator({ fastHalfLife: 3, slowHalfLife: 9, defaultEstimate: 1_000_000 }); const selector = createThroughputSelector(); // First request. The estimate is the 1 Mbps default, so the selector picks rung 1 (750 kbps). const first = selector.select({ ladder, bufferLevel: 0, throughput: estimator.getEstimate(), now: 0 }); // The player downloads a 4 second segment of rung 1 (375,000 bytes) in 500 ms, at 6 Mbps. const timing = { startTime: 0, encodedBodySize: 375_000, duration: 500 }; estimator.sample(timing); selector.sample({ timing, bandwidth: ladder[first.index].bandwidth, duration: 4, bufferLevel: 0 }); // Second request. The estimate is about 2.7 Mbps, so the selector picks rung 2 (1.5 Mbps). const second = selector.select({ ladder, bufferLevel: 4, throughput: estimator.getEstimate(), now: 500, segmentDuration: 4 }); ``` The player follows these rules: - `index` is a position in the `ladder` array of the same call. The player sorts the ladder by `bandwidth` in ascending order and removes the rungs it cannot play before the call. - Pass the declared peak bandwidth of each rung: `BANDWIDTH` in HLS and `@bandwidth` in DASH. - Call `select` after the `sample` of the previous segment and before the next request, not during a download. A second call before the request replaces the first decision. - If the chosen rung is unavailable, remove it from the ladder and call `select` again. - `delay` is the time in seconds to wait before the next request. The throughput selector always returns 0. A player that cannot wait may ignore the value. After a delay, call `select` again. The selector accounts for the wait. - Pass optional fields that configure a selector, such as `bufferTarget`, in every call or in none. - Call `sample` for every finished download, and for every abandoned download with `aborted: true`. The `bandwidth` of the sample is the same number as the ladder entry of the requested rung, never a measured rate. Whether an aborted download produces an estimator sample is the contract of `ThroughputEstimator`, not of the selector. - Call `reset` after a seek or a source change. ### Input validation | Input | Behavior | |---|---| | `ladder` empty | Throws a `RangeError` that names the parameter. | | An entry whose `bandwidth` is not a positive finite number | Throws a `RangeError` with the position and the value. | | `ladder` not in ascending order of `bandwidth` | Throws a `RangeError` with both positions and values. | | Equal neighbouring `bandwidth` values | Accepted. The highest index wins. | | `throughput` not a finite number of 0 or more | Treated as unknown. | | `bufferLevel` not finite or negative | Treated as 0. | | `playbackRate` not a positive finite number | Treated as 1. | | `segmentDuration` not a positive finite number | Treated as not passed. | ## Throughput selector The selector divides the throughput by `playbackRate` to get the effective throughput. It picks the highest rung at or below `effectiveThroughput * safetyFactor`. When no rung is at or below that value, the pick is rung 0. Two rules refine the pick: - **Hysteresis.** An upgrade needs a larger margin than a downgrade. If the pick is above the current rung, the selector recomputes the pick with `effectiveThroughput * upgradeFactor`. It then returns the higher of the current rung and that pick. The current rung is the bandwidth of the last `sample` that was not aborted. The selector maps that bandwidth to the highest rung at or below it in the ladder of the call. When no rung qualifies, the current rung is rung 0. - **Starvation guard.** The guard runs when the player passes `segmentDuration`. The selector estimates the download time of a rung as `bandwidth * segmentDuration / (throughput * safetyFactor)`, with the network throughput and not the effective throughput. The first pass keeps the highest rung, at or below the pick, whose download time is within `bufferLevel / playbackRate`. If no rung passes, the second pass adds `min(segmentDuration, starvationTolerance)` to that budget and keeps the highest rung within it. Rung 0 passes the second pass in every case. A live player omits `segmentDuration` to skip the guard, as hls.js skips the check near the live edge. When the throughput is unknown, the selector returns the current rung, or rung 0 before the first sample. A throughput of 0 is a measurement, so the pick is rung 0. The delay is always 0. With the ladder of the examples and a current rung of 1.5 Mbps, the hysteresis gives these picks: | Throughput | 1.6 Mbps | 1.7 Mbps | 4.2 Mbps | 4.3 Mbps | 8.6 Mbps | |---|---|---|---|---|---| | Rung | 750 kbps | 1.5 Mbps | 1.5 Mbps | 3 Mbps | 6 Mbps | The downgrade happens when the safe throughput falls below the current rung, at 1.67 Mbps. The upgrade to 3 Mbps needs `3 / 0.7`, which is 4.29 Mbps. With a current rung of 6 Mbps, a throughput of 6 Mbps, and 4 second segments, the guard gives these picks: | Buffer level | 8 s | 1.5 s | 0.5 s | 0.1 s | 0 s | |---|---|---|---|---|---| | Rung | 3 Mbps | 1.5 Mbps | 300 kbps | 3 Mbps | 3 Mbps | The safe throughput of 5.4 Mbps does not sustain the 6 Mbps rung, so the pick starts at 3 Mbps. That rung downloads in 2.2 s and fails the first pass below that buffer level. Below 0.2 s no rung passes the first pass, and the second pass allows 4 s more. The step from 300 kbps to 3 Mbps is the policy of the two passes. The first pass avoids a stall when any rung can. The second pass takes the best rung within the tolerance. The playback rate changes the pick before the hysteresis and the guard run: | Playback rate | Throughput | Effective throughput | Pick | |---|---|---|---| | 1 | 5 Mbps | 5 Mbps | 3 Mbps | | 2 | 5 Mbps | 2.5 Mbps | 1.5 Mbps | ### Options ```typescript import { createThroughputSelector } from "@svta/cml-abr"; const selector = createThroughputSelector({ safetyFactor: 0.95, upgradeFactor: 0.85, starvationTolerance: 2 }); ``` | Option | Default | Description | |---|---|---| | `safetyFactor` | `DEFAULT_SAFETY_FACTOR` (0.9) | The fraction of the effective throughput that a rung may use. | | `upgradeFactor` | `DEFAULT_UPGRADE_FACTOR` (0.7) | The fraction of the effective throughput that a higher rung may use. | | `starvationTolerance` | `DEFAULT_STARVATION_TOLERANCE` (4) | The seconds of stall that the second pass of the guard allows, capped at the segment duration. | Both factors must be finite numbers above 0 and at most 1. `upgradeFactor` must not exceed `safetyFactor`, because a larger upgrade factor could pass the safe pick and the next call would undo it. Equal factors disable the hysteresis. The tolerance must be a finite number of 0 or more. `createThroughputSelector` throws a `RangeError` for an invalid option. The defaults follow the players: | Setting | CML default | hls.js | shaka-player | dash.js | |---|---|---|---|---| | Safety factor | 0.9 | 0.95 (`abrBandWidthFactor`) | 0.95 (`bandwidthDowngradeTarget`) | 0.9 (`bandwidthSafetyFactor`) | | Upgrade factor | 0.7 | 0.7 (`abrBandWidthUpFactor`) | 0.85 (`bandwidthUpgradeTarget`) | none | | Starvation guard | two passes | two passes | no | separate rule | | Starvation tolerance | 4 s | 4 s (`maxStarvationDelay`) | none | none | | Minimum switch interval | none | none | 8 s (`switchInterval`) | none | The shaka-player targets are band edges around each rung and do not depend on the current rung. Its hysteresis is the switch interval. ## BOLA selector `createBolaSelector` creates a buffer-based selector. It implements BOLA, the Buffer Occupancy based Lyapunov Algorithm of Spiteri, Urgaonkar, and Sitaraman. A full buffer allows a high rung. A low buffer forces a low rung. The selector needs the throughput estimate at startup and in its two guards. ```typescript import { createBolaSelector } from "@svta/cml-abr"; const selector = createBolaSelector(); const ladder = [{ bandwidth: 300_000 }, { bandwidth: 750_000 }, { bandwidth: 1_500_000 }, { bandwidth: 3_000_000 }, { bandwidth: 6_000_000 }]; // First request. No sample exists and the estimate is unknown, so the selector picks rung 0. const first = selector.select({ ladder, bufferLevel: 0, throughput: NaN, now: 0 }); // The player downloads a 4 second segment of rung 0 (150,000 bytes) in 400 ms, at 3 Mbps. const timing = { startTime: 0, encodedBodySize: 150_000, duration: 400 }; selector.sample({ timing, bandwidth: ladder[first.index].bandwidth, duration: 4, bufferLevel: 0 }); // Second request. The estimate is 3 Mbps, so the selector picks rung 2 (1.5 Mbps). const second = selector.select({ ladder, bufferLevel: 4, throughput: 3_000_000, now: 400 }); console.log(second.index, second.state); // 2 steady ``` The decision has the `index` and the `delay` of every selector. It also has the `state` of BOLA and the `placeholder` buffer in seconds. The player follows the shared rules of the [player integration](#player-integration) section. BOLA adds these rules: - `delay` is positive when the buffer is above the maximum buffer of the chosen rung. The top rung never has a delay. Wait, and call `select` again. The selector adds the wait to its placeholder buffer. If the buffer drained during the wait, the second call returns a delay of 0. If it did not drain, for example in a paused player, the second call returns the delay again. A player that ignores the delay stays at the selected rung until the real buffer passes the next threshold. The cost is lower quality, not a stall. - `bufferTarget` is the buffer level, in seconds, that the player aims for. A larger value spreads the thresholds over a larger range of buffer levels. The default is `BOLA_DEFAULT_BUFFER_TARGET` (18). Pass the field in every call or in none. - `segmentDuration` enables the starvation guard. The guard lowers the pick when the download of the next segment does not fit in the real buffer. A live player omits the field to skip the guard, as for the throughput selector. - A stall needs no call. The selector treats a `bufferLevel` of 0.3 seconds or less as a stall in the steady state. - A selector that never receives a `sample` stays in the startup state. A `sample` without a positive finite `duration` has the same effect. - Create one selector for each media type, such as audio and video. - The BOLA rules ignore `playbackRate`. The starvation guard reads `segmentDuration` and `playbackRate`. ### How it works BOLA scores each rung with the effective buffer. The effective buffer is the real buffer level plus a placeholder buffer. The placeholder is virtual buffer time. It lets startup and seeks begin at a sustainable rung, not at the lowest rung. The rung with the best score wins. The selector turns this into one threshold for each rung, so a call does not compute scores. | State | Behavior | |---|---| | Startup | Picks the highest rung at or below `throughput * safetyFactor`, or rung 0 when the estimate is unknown. Sets the placeholder so that the effective buffer is at least the threshold of that rung. Moves to the steady state in the first call after a `sample`, when the buffer holds one segment. That call still picks from the throughput. | | Steady | Adds the wall-clock time since the last request ended to the placeholder. Picks the rung for the effective buffer. Applies the oscillation guard and the starvation guard. Returns a delay when the buffer is above the maximum buffer of the rung. | The oscillation guard is the BOLA-O variant. Suppose the buffer picks a rung above both the current rung and the rung that the throughput sustains. Then the selector keeps the higher of those two rungs. The guard never forces a downward switch, and it is skipped when the estimate is unknown. ### Starvation guard The starvation guard is the guard of the throughput selector, with one pass. It runs in the steady state, after the oscillation guard and before the delay. It runs when all of these conditions hold: - The input has a positive `segmentDuration`. - The throughput is known. - Two samples have finished since the last `reset`. An aborted sample does not count. The guard estimates the download time of a rung as `bandwidth * segmentDuration / (throughput * starvationSafetyFactor)`. It keeps the highest rung, at or below the pick, whose download time is within `bufferLevel / playbackRate`. Rung 0 always passes. The guard never raises a pick. A lowered pick becomes the current rung, and the call returns no delay. The guard has its own factor. One download varies more than the smoothed estimate, so the default is 0.7 and not 0.9. The guard binds only when the real buffer is low, so the larger margin costs little bitrate. Take the ladder of the examples, a throughput of 4 Mbps, and 4 second segments. For a pick of 6 Mbps, the guard gives these picks: | Buffer level | 8 s | 6 s | 3 s | 1.5 s | 0.5 s | |---|---|---|---|---|---| | Rung | 3 Mbps | 3 Mbps | 1.5 Mbps | 750 kbps | 300 kbps | The 3 Mbps rung needs 4.3 s to download at the safe throughput of 2.8 Mbps. It fits in a buffer of 6 s and not in a buffer of 3 s. At 0.5 s only rung 0 fits. ### What `sample` does The selector reads these fields of a `SegmentSample`: | Field | Use | |---|---| | `bandwidth` | Sets the current rung. For an aborted sample, it locates the aborted rung. The selector ignores a value that is not finite. | | `timing` | The end of the request, `startTime + duration`, starts the clock of the placeholder. An aborted request ends at the abort. The request `duration` gives the buffer level at the start of the request. | | `duration` | Moves the selector to the steady state, and sizes the growth for a replacement segment. A value that is not a positive finite number keeps the startup state. | | `bufferLevel` | Decays and caps the placeholder. In the steady state, 0.3 seconds or less is a stall. | | `isReplacement` | Adds `duration` to the placeholder. | | `aborted` | The next lower rung replaces the aborted rung. It becomes the current rung, and its threshold caps the placeholder. When the replacement is rung 0, the cap is 10 seconds. For an abort of rung 0 the current rung stays. The stall rule applies. A finished sample counts for the starvation guard, and an aborted sample does not. | ### Ladder changes Pass the ladder and the `bufferTarget` in every call. The selector compares them with the values of the last call, and it detects a change made in place. A multi-period stream, a new bitrate limit, or a new video size needs no other call. The player filters the ladder and the selector adapts. In the steady state, the selector adds the elapsed time to the placeholder and then converts its effective buffer. The pick for the same buffer level then stays the same. One case differs. The placeholder cannot be negative. If the conversion needs a negative placeholder, the placeholder is 0 and the real buffer alone decides. A larger `bufferTarget` with a small placeholder causes this. When the ladder gains or loses its second distinct rung, the selector returns to the startup state. It also forgets the duration of the last segment, so it stays in that state until the next `sample`. After a seek or a source change, call `reset`. ### Comparison with the throughput selector | | Throughput selector | BOLA selector | |---|---|---| | Main input | The throughput estimate | The buffer level | | Throughput estimate | Needed in every call | Needed at startup and in the two guards | | State | The current rung | The state, the placeholder buffer, and the time bases | | Delay | Always 0 | Positive above the maximum buffer of the rung | | Starvation guard | Two passes, with `starvationTolerance` | One pass, with `starvationSafetyFactor` | A seeded simulation ran both selectors on 100 random throughput traces. Each session has 75 segments of 4 seconds, and the player passes `segmentDuration`. The BOLA selector uses its default options. The ladders are the five rungs of the examples and a ladder of ten rungs from 235 kbps to 5.8 Mbps. | Ladder and buffer cap | Selector | Average Mbps | Stalls per session | Switches per session | |---|---|---|---|---| | 5 rungs, 18 s | BOLA | 1.87 | 0.13 | 5.7 | | 5 rungs, 18 s | Throughput | 1.28 | 0.03 | 5.3 | | 10 rungs, 18 s | BOLA | 2.21 | 0.29 | 11.5 | | 10 rungs, 18 s | Throughput | 1.62 | 0.02 | 10.5 | | 10 rungs, 30 s | BOLA | 2.30 | 0.16 | 10.4 | | 10 rungs, 30 s | Throughput | 1.61 | 0.02 | 10.2 | BOLA picks higher rungs, and its average bitrate is 36% to 47% above the throughput selector. It also stalls more often, even with the starvation guard. Without the guard, BOLA stalls 0.23 to 0.96 times per session in the same runs. A lower `starvationSafetyFactor` trades bitrate for fewer stalls. The model is simple. The numbers compare the selectors under one channel model, and they do not predict a real session. ### Limits - **Buffer size.** BOLA suits a large buffer. In the [dash.js follow-up paper](https://doi.org/10.1145/3204949.3204953), a throughput rule oscillates less than the original BOLA with a buffer of 10 seconds. The original BOLA has no placeholder buffer and no starvation guard. The selector has both. The dash.js player still runs BOLA only above a buffer level, so a small `bufferTarget` needs care. The selector does not warn about it. A player can combine the selector with a rate-based rule at low buffer levels. - **Real buffer.** The placeholder adds to the real buffer. BOLA can then pick a rung that the real buffer alone would not sustain. The starvation guard limits this risk when the player passes `segmentDuration`. A player that omits the field owns the stall risk. With the default guard, BOLA still stalls more often than the throughput selector in the simulation above. - **Lowest rungs.** The formula can give a threshold below 0, and the selector uses 0 instead. Then the buffer rule never picks a rung below the highest rung with a threshold of 0. The rule holds even at an empty buffer. Three cases cause this: - A ladder with many rungs. Each distinct rung adds 2 seconds to the smallest buffer time, which is `10 + 2 * M` seconds for `M` rungs. A test ladder of 20 rungs with equal bandwidth ratios never picks its two lowest rungs. - A large `bufferTarget`. - Two lowest rungs with close bandwidths. A ladder of two rungs with the default `bufferTarget` always picks the upper rung when the bandwidth ratio is 1.8 or less. - **Playback rate.** The BOLA rules ignore `playbackRate`, as dash.js does. The starvation guard divides the buffer level by it. ### BOLA options ```typescript import { createBolaSelector } from "@svta/cml-abr"; const selector = createBolaSelector({ safetyFactor: 0.95, starvationSafetyFactor: 0.5 }); ``` | Option | Default | Description | |---|---|---| | `safetyFactor` | `DEFAULT_SAFETY_FACTOR` (0.9) | The fraction of the throughput that a rung may use. The selector uses it at startup and in the oscillation guard. | | `starvationSafetyFactor` | `DEFAULT_STARVATION_SAFETY_FACTOR` (0.7) | The fraction of the throughput that the starvation guard assumes for the download of the next segment. | `safetyFactor` has the bounds of the option with the same name of `createThroughputSelector`. `starvationSafetyFactor` has the same bounds. Each factor must be a finite number above 0 and at most 1. `createBolaSelector` throws a `RangeError` for an invalid value. ### BOLA input validation The rows of the [input validation](#input-validation) table apply. A ladder with one distinct bandwidth gets that rung and a delay of 0. The state is startup and the placeholder is 0. BOLA adds two rows: | Input | Behavior | |---|---| | `bufferTarget` not a positive finite number | Throws a `RangeError` that names the parameter, the expected value, and the received value. | | `bufferTarget` below `10 + 2 * M` | Raised to `10 + 2 * M`. | ### Differences from dash.js The selector follows `BolaRule.js` of dash.js, with these changes: - The minimum and maximum buffer helpers use the same utility offset as the score. The dash.js helpers differ from the score by `V` seconds. - The selector reads the ladder in every call and handles a change made in place. The dash.js player builds its state once for each stream and media type. - The selector indexes everything by position in the player ladder. The dash.js player mixes positions of a filtered list with indices of an unfiltered list. - `M` counts the distinct bandwidths. dash.js counts the representations. - An unknown throughput does not block upward switches in the steady state, and it picks rung 0 at startup. - The selector derives a stall from the buffer level and clears the time bases of the placeholder. - The player passes the time. dash.js reads `Date.now()`. - A remainder of one millisecond or less is not a delay. - After an aborted download, the selector treats the next lower rung as the replacement. The dash.js player uses the rung that its abandon rule picks from the throughput. The selector input has no replacement rung. Both set the current rung to the replacement. Both cap the effective buffer at the threshold of that rung, or at 10 seconds when the replacement is rung 0. - After a change of the buffer target, the selector adds the elapsed time and then converts the effective buffer. The dash.js player converts first and then adds the elapsed time. - The starvation guard is part of the selector. The dash.js player runs `InsufficientBufferRule` as a separate rule. It has the same cap, `safeThroughput * bufferLevel / segmentDuration`, the same factor of 0.7, and the same wait of two segments. The dash.js player disables that rule in low-latency mode, and a player of the selector omits `segmentDuration` instead. After a buffer-empty event, the dash.js player drops to rung 0. The selector empties the placeholder, and the guard at an empty buffer allows rung 0 only. ## References - [Bitrate selector RFC](https://github.com/streaming-video-technology-alliance/common-media-library/pull/526) - [hls.js `AbrController`](https://github.com/video-dev/hls.js/blob/master/src/controller/abr-controller.ts) - [BOLA selector RFC](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/rfc/bola-abr-selector/index.md) - [BOLA: Near-Optimal Bitrate Adaptation for Online Videos](https://arxiv.org/abs/1601.06748) - [From Theory to Practice: Improving Bitrate Adaptation in the DASH Reference Player](https://doi.org/10.1145/3204949.3204953) - [dash.js `BolaRule`](https://github.com/Dash-Industry-Forum/dash.js/blob/ac9e3d18818f3ba9b99a87151b887d66aed12c56/src/streaming/rules/abr/BolaRule.js) - [dash.js `InsufficientBufferRule`](https://github.com/Dash-Industry-Forum/dash.js/blob/ac9e3d18818f3ba9b99a87151b887d66aed12c56/src/streaming/rules/abr/InsufficientBufferRule.js) - [dash.js `ThroughputRule`](https://github.com/Dash-Industry-Forum/dash.js/blob/ac9e3d18818f3ba9b99a87151b887d66aed12c56/src/streaming/rules/abr/ThroughputRule.js) - [shaka-player `AbrManager`](https://github.com/shaka-project/shaka-player/blob/main/externs/shaka/abr_manager.js) ## API Report File for "@svta/cml-abr" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ResourceTiming } from '@svta/cml-utils'; import { ValueOf } from '@svta/cml-utils'; // @beta export type BitrateDecision = { readonly index: number; readonly delay: number; }; // @beta export type BitrateSelector = { select(input: BitrateSelectorInput): BitrateDecision; sample(segment: SegmentSample): void; reset(): void; }; // @beta export type BitrateSelectorInput = { ladder: readonly LadderRung[]; bufferLevel: number; throughput: number; now: number; bufferTarget?: number; segmentDuration?: number; playbackRate?: number; }; // @beta export const BOLA_DEFAULT_BUFFER_TARGET = 18; // @beta export type BolaDecision = BitrateDecision & { readonly state: BolaState; readonly placeholder: number; }; // @beta export type BolaSelector = Omit & { select(input: BitrateSelectorInput): BolaDecision; }; // @beta export type BolaSelectorOptions = { safetyFactor?: number; starvationSafetyFactor?: number; }; // @beta export const BolaState: { readonly STARTUP: "startup"; readonly STEADY: "steady"; }; // @beta (undocumented) export type BolaState = ValueOf; // @beta export function createBolaSelector(options?: BolaSelectorOptions): BolaSelector; // @beta export function createThroughputSelector(options?: ThroughputSelectorOptions): BitrateSelector; // @beta export const DEFAULT_SAFETY_FACTOR = .9; // @beta export const DEFAULT_STARVATION_SAFETY_FACTOR = .7; // @beta export const DEFAULT_STARVATION_TOLERANCE = 4; // @beta export const DEFAULT_UPGRADE_FACTOR = .7; // @beta export type LadderRung = { bandwidth: number; }; // @beta export type SegmentSample = { timing: ResourceTiming; bandwidth: number; duration: number; bufferLevel: number; isReplacement?: boolean; aborted?: boolean; }; // @beta export type ThroughputSelectorOptions = { safetyFactor?: number; upgradeFactor?: number; starvationTolerance?: number; }; // (No @packageDocumentation comment for this package) ``` # @svta/cml-c2pa C2PA (Coalition for Content Provenance and Authenticity) live video validation for BMFF and MP4 containers. Supported C2PA segment validation methods: - **§19.3 Per-segment C2PA Manifest Box**: each segment embeds a full C2PA manifest with a COSE signature - **§19.4 Verifiable Segment Info (VSI/EMSG)**: the init segment contains the manifest and the session keys. Each media segment contains a small signed EMSG box - **§15.12.2 / §18.6 VOD Merkle**: for VOD assets in fragmented MP4, the init manifest stores one Merkle tree row per track. Each media segment contains its own leaf hash and proof ## When to use - Validate the C2PA manifest in an init segment: `validateC2paInitSegment` - Validate a live segment with the VSI and EMSG method and the session keys of the init segment: `validateC2paSegment` - Validate a per-segment C2PA manifest box: `validateC2paManifestBoxSegment` - Validate the Merkle leaf and proof in a VOD segment: `validateC2paMerkleSegment` - Map a validation result to a status code: `C2paStatusCode`, `LiveVideoStatusCode` Do not write your own COSE, CBOR, or manifest box parsing for C2PA. Open an issue for a validation method that is missing. ## Installation ```bash npm i @svta/cml-c2pa ``` > **Note:** This library uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) (`crypto.subtle`) to verify COSE signatures and compute BMFF hashes. In Node.js 20+, `crypto.subtle` is available globally. In browsers, it requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS or `localhost`). ## Usage ### Manifest Box method (per-segment manifests) ```typescript import { validateC2paManifestBoxSegment } from '@svta/cml-c2pa' import type { ManifestBoxValidationState } from '@svta/cml-c2pa' async function validateManifestBoxStream(segmentUrls: string[]): Promise { let lastManifestId: string | null = null let state: ManifestBoxValidationState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const { result, nextManifestId, nextState } = await validateC2paManifestBoxSegment( bytes, lastManifestId, state, ) lastManifestId = nextManifestId state = nextState console.log(result.isValid, result.errorCodes) } } ``` ### VSI/EMSG method (init segment + media segments) ```typescript import { validateC2paInitSegment, validateC2paSegment } from '@svta/cml-c2pa' async function validateVsiSegment(initUrl: string, segmentUrl: string): Promise { const initResponse = await fetch(initUrl) const init = await validateC2paInitSegment(new Uint8Array(await initResponse.arrayBuffer())) if (!init.isValid) { console.error(init.errorCodes) return } const segmentResponse = await fetch(segmentUrl) const segmentBytes = new Uint8Array(await segmentResponse.arrayBuffer()) const validated = await validateC2paSegment(segmentBytes, init.sessionKeys) console.log(validated?.result.isValid) } ``` ### VOD Merkle method (init segment + media segments) ```typescript import { validateC2paInitSegment, validateC2paMerkleSegment } from '@svta/cml-c2pa' async function validateMerkleSegment(initUrl: string, segmentUrl: string): Promise { const initResponse = await fetch(initUrl) const init = await validateC2paInitSegment(new Uint8Array(await initResponse.arrayBuffer())) if (!init.isValid) { console.error(init.errorCodes) return } const segmentResponse = await fetch(segmentUrl) const segmentBytes = new Uint8Array(await segmentResponse.arrayBuffer()) const { result } = await validateC2paMerkleSegment(segmentBytes, init.merkleMaps) console.log(result.isValid, result.errorCodes) } ``` ## Docs - [VSI/EMSG Validation](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/libs/c2pa/docs/vsi-validation.md) (§19.4) - [Manifest Box Validation](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/libs/c2pa/docs/manifest-box-validation.md) (§19.3) - [VOD Merkle Validation](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/libs/c2pa/docs/merkle-validation.md) (§15.12.2 / §18.6) - [Results and Error Codes](https://github.com/streaming-video-technology-alliance/common-media-library/blob/main/libs/c2pa/docs/results-and-error-codes.md) ## References - [C2PA Specification v2.3](https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html) - [JUMBF (JPEG Universal Metadata Box Format), ISO 19566-5](https://www.iso.org/standard/84635.html) - [COSE, RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) --- title: Manifest Box Validation description: Validate live streams using per-segment C2PA Manifest Boxes (C2PA section 19.3) --- # Manifest Box Validation The Manifest Box method embeds a full C2PA manifest in each media segment, as section 19.3 of the C2PA specification defines. Each segment is self-contained, with its own COSE signature, so it needs no init segment and no session keys. Manifest ID chaining verifies the continuity between segments. See Manifest ID Chaining below. ## Overview Unlike the [VSI/EMSG method](https://cml.svta.org/llms/c2pa/docs/vsi-validation.md), the Manifest Box method needs no separate init segment validation step. If you validate the init segment of a section 19.3 stream with `validateC2paInitSegment`, the function accepts it without a `c2pa.session-keys` assertion. One function, `validateC2paManifestBoxSegment`, does everything: manifest parsing, signature verification, BMFF hash validation, and continuity checks. Two pieces of state pass from one call to the next: - `lastManifestId`: the manifest ID from the last segment that updated the state, used for the continuity check - `state`: a `ManifestBoxValidationState` object that tracks `lastStreamId` and `lastSequenceNumber` ## Validating Segments ```typescript import { validateC2paManifestBoxSegment } from '@svta/cml-c2pa' import type { ManifestBoxValidationState } from '@svta/cml-c2pa' let lastManifestId: string | null = null let state: ManifestBoxValidationState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const { result, nextManifestId, nextState } = await validateC2paManifestBoxSegment( bytes, lastManifestId, state, ) lastManifestId = nextManifestId state = nextState if (result.isValid) { console.log(`Segment ${result.sequenceNumber}: valid`) } else { console.error(`Segment ${result.sequenceNumber}: failed`, result.errorCodes) } } ``` The function returns an object with: - `result`: a `ManifestBoxValidationResult` with the validation outcome - `nextManifestId`: the manifest ID to pass into the next call - `nextState`: the state to pass into the next call > [!NOTE] > For the first segment, pass `null` as `lastManifestId`, and `undefined` or nothing for `state`. When `lastManifestId` is `null`, the validator skips the chain comparison with the previous segment. It still checks that the segment declares a valid `continuityMethod` and a non-empty `previousManifestId`. ## Complete Example ```typescript import { validateC2paManifestBoxSegment, LiveVideoStatusCode } from '@svta/cml-c2pa' import type { ManifestBoxValidationState } from '@svta/cml-c2pa' async function validateStream(segmentUrls: string[]) { let lastManifestId: string | null = null let state: ManifestBoxValidationState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const { result, nextManifestId, nextState } = await validateC2paManifestBoxSegment( bytes, lastManifestId, state, ) lastManifestId = nextManifestId state = nextState if (result.isValid) { console.log( `Segment ${result.sequenceNumber}: valid` + ` (stream: ${result.streamId}, issuer: ${result.issuer})`, ) } else { for (const code of result.errorCodes) { switch (code) { case LiveVideoStatusCode.CONTINUITY_METHOD_INVALID: console.error('Continuity chain broken') break case LiveVideoStatusCode.ASSERTION_INVALID: console.error('Assertion field mismatch (sequenceNumber or streamId)') break default: console.error('Validation failure:', code) } } } // Access the full manifest when needed. Its assertions list is the raw store, so check isValid first. if (result.isValid && result.manifest) { console.log('Claim generator:', result.manifest.claimGenerator) console.log('Assertions:', result.manifest.assertions.length) } } } ``` ## State Management The `ManifestBoxValidationState` object contains the state between segments for the continuity checks: | Field | Type | Description | |-------|------|-------------| | `lastStreamId` | `string \| null` | Stream ID from the last segment that updated the state | | `lastSequenceNumber` | `number \| null` | Sequence number from the last segment that updated the state | The function is pure. It does not mutate any external state. The caller must keep `nextManifestId` and `nextState` between calls. A segment with an integrity error, a malformed or mismatched content hash, or no live video assertion does not change `nextManifestId` or `nextState`. The same applies to a segment on another stream or with a sequence number that does not move forward. A segment that fails only the continuity check still updates both values. After a seek, an encoder restart, or a new stream ID, pass `null` as `lastManifestId` and `undefined` as `state` to start a new state. If your application rejects the signer of a segment, discard the `nextManifestId` and `nextState` of that call. When `state` is omitted or `undefined`, the function skips the checks for a consistent stream ID and for increasing sequence numbers. ## Manifest ID Chaining The `c2pa.livevideo.segment` assertion of each segment includes a `previousManifestId` field. That field must match the manifest ID of the previous segment, which creates a verifiable chain: ``` Segment 1: manifestId = "urn:c2pa:1b3b...", previousManifestId = "urn:c2pa:f916..." Segment 2: manifestId = "urn:c2pa:2e52...", previousManifestId = "urn:c2pa:1b3b..." // must match segment 1 Segment 3: manifestId = "urn:c2pa:3000...", previousManifestId = "urn:c2pa:2e52..." // must match segment 2 ``` The manifest ID is the manifest identifier of C2PA, a `urn:c2pa:` URN. The manifest stores it as the JUMBF label of the manifest box. The claim `instanceID` is a different value and does not take part in the chain. The validator compares the two manifest IDs exactly, without normalization. The JUMBF label is outside the signed claim. The claim `signature` field is a JUMBF URI of the signature box (C2PA section 8.4.2.1). The validator uses the label as the manifest ID only if this signed URI names it: - An absolute URI, such as `self#jumbf=/c2pa/urn:c2pa:1b3b.../c2pa.signature`, names the label. Only a URI that starts with a slash names it. - A URI relative to the manifest, `self#jumbf=c2pa.signature`, resolves to the signature box but does not name the label. The validator cannot verify the manifest ID chain, so the segment fails with `LiveVideoStatusCode.SEGMENT_INVALID`, whatever `lastManifestId` you pass. The rule applies to every continuity method, including a custom one. The segment does not update `nextManifestId` or `nextState`. - A URI that does not resolve to the signature box of the same manifest fails with `C2paStatusCode.CLAIM_SIGNATURE_MISSING`. The segment does not update the state. > [!NOTE] > Every segment must declare a `previousManifestId`, including the first one. The first segment usually references the initial static manifest. The `continuityMethod` field states how continuity is verified. The only built-in method is `c2pa.manifestId`, which the specification defines (C2PA §19.3.2). When the chain is broken, because `previousManifestId` does not match the manifest ID of the previous segment, the result includes `LiveVideoStatusCode.SEGMENT_INVALID`. ### Custom continuity methods The specification allows implementers to define their own continuity methods (§19.3.2), identified by a custom label such as `com.example.anchor-chain`. Their semantics are implementation-specific, so this library cannot validate them by default. Segments that declare an unrecognized method fail with `LiveVideoStatusCode.CONTINUITY_METHOD_INVALID`, as §19.7.2 requires. They also get `LiveVideoStatusCode.CONTINUITY_METHOD_UNSUPPORTED`, which lets consumers tell an unverifiable method from a broken chain. To validate a custom method, register a validator for its label in the `options` parameter. A stream uses one continuity method, so one validator is registered at a time: ```ts import { validateC2paManifestBoxSegment } from '@svta/cml-c2pa' const { result } = await validateC2paManifestBoxSegment(bytes, lastManifestId, state, { continuityValidator: { method: 'com.example.anchor-chain', validate: (liveVideoAssertion, manifest) => { // method-specific fields are available on liveVideoAssertion return verifyAnchorChain(liveVideoAssertion['anchorToken'], manifest.instanceId) }, }, }) ``` The validator receives the full decoded `c2pa.livevideo.segment` assertion, including method-specific fields, and the parsed manifest of the segment. Anything else the method needs, such as `lastManifestId`, which the caller already has, can be captured in the closure of the callback. Returning `false` or throwing reports `LiveVideoStatusCode.SEGMENT_INVALID`. The built-in `c2pa.manifestId` method cannot be overridden. ## Result Fields The `ManifestBoxValidationResult` contains: | Field | Type | Description | |-------|------|-------------| | `manifest` | `C2paManifest \| null` | Parsed manifest, or `null` on parse failure | | `issuer` | `string \| null` | Certificate issuer from the signature | | `certificate` | `Uint8Array \| null` | DER-encoded end-entity certificate from the claim signature, or `null` when the signature is absent or carries no certificate | | `sequenceNumber` | `number \| null` | From the `c2pa.livevideo.segment` assertion, or `null` if the value is not an unsigned integer up to `Number.MAX_SAFE_INTEGER` or the claim does not verify the assertion | | `previousManifestId` | `string \| null` | From the `c2pa.livevideo.segment` assertion, or `null` if the claim does not verify it | | `streamId` | `string \| null` | From the `c2pa.livevideo.segment` assertion, or `null` if the claim does not verify it | | `continuityMethod` | `string \| null` | From the `c2pa.livevideo.segment` assertion, or `null` if the claim does not verify it | | `bmffHashHex` | `string \| null` | Hex-encoded hash from the `c2pa.hash.bmff.v3` assertion, or `null` if the claim does not verify it | | `isValid` | `boolean` | All checks passed | | `errorCodes` | `readonly (LiveVideoStatusCode \| C2paStatusCode)[]` | Failure codes (empty when valid) | > [!NOTE] > Unlike the VSI method, the Manifest Box method can produce both `LiveVideoStatusCode` and `C2paStatusCode` error codes. Each segment contains a full manifest. The library runs these integrity checks on it: assertion references and hashes, undeclared assertions, the hard binding count, duplicate manifest boxes, and claim signature verification. The segment fields above come only from assertions whose hash matches a claim reference. Check `isValid` before you use any field. A `ManifestBoxContinuityValidator` receives the parsed manifest, whose `assertions` list is the raw assertion store. > > A segment without a `c2pa.signature` box fails with `C2paStatusCode.CLAIM_SIGNATURE_MISSING`. The claim signature is verified with the certificate inside the segment. The library does not check that certificate against a trust list. See [Signer Trust](https://cml.svta.org/llms/c2pa/docs/results-and-error-codes.md#signer-trust). > > C2PA section 19.3 requires a `c2pa.hash.bmff.v3` assertion in the manifest of each segment. A segment without that assertion fails with `LiveVideoStatusCode.SEGMENT_INVALID`. If the manifest also has no other hard binding assertion, the result also includes `C2paStatusCode.CLAIM_HARD_BINDINGS_MISSING`. The library adds that code only for a claim that decodes. A manifest with a claim code such as `C2paStatusCode.CLAIM_MISSING` does not also report `CLAIM_HARD_BINDINGS_MISSING`. > > A `c2pa.hash.bmff.v3` assertion that is not a map, has no `hash`, or has a `hash` that is not a byte string fails with `C2paStatusCode.ASSERTION_BMFFHASH_MALFORMED`. The validator does not read a `value` field in place of `hash`. An assertion with an exclusion that does not conform to C2PA section 18.6 fails with the same code. Each `data` constraint of an exclusion needs an unsigned integer `offset` and a byte string `value`. The `xpath` of an exclusion must follow the grammar of section 18.6.2. Each node is a `/`, a four-character box type, and an optional index such as `[2]`. The validator also checks the `length`, `subset`, `version`, `flags`, and `exact` fields against the CDDL of section 18.6.2. --- title: VOD Merkle Validation description: Validate on-demand fragmented MP4 assets using Merkle tree proofs (C2PA section 15.12.2 / 18.6) --- # VOD Merkle Validation The VOD Merkle method (C2PA section 15.12.2.2 / 18.6) validates fragmented MP4 (fMP4) assets, such as HLS or DASH VOD streams. The manifest does not list every segment hash. Instead, the init manifest stores one row of a Merkle tree per track. A Merkle tree is a hash tree: each row is computed from the row below. Each media segment has a small auxiliary `uuid` box. The box gives the leaf index of the segment (`location`) and the sibling hashes that derive the stored row from the leaf hash. Unlike the [Manifest Box](https://cml.svta.org/llms/c2pa/docs/manifest-box-validation.md) and [VSI/EMSG](https://cml.svta.org/llms/c2pa/docs/vsi-validation.md) methods, VOD Merkle segments have no manifest or signature of their own. The manifest of the init segment is the only source of trust. ## Overview Validation happens in two steps: 1. `validateC2paInitSegment` extracts the `merkleMaps`, one per track, from the `c2pa.hash.bmff.v3` assertion of the init segment. It also validates the `initHash` binding of each entry against the bytes of the init segment. 2. `validateC2paMerkleSegment` verifies the Merkle proof of each media segment against those `merkleMaps`. It also enforces `location` continuity per track, with state that the caller keeps. A stream is in VOD Merkle mode only when `merkleMaps` is not empty. Otherwise it uses one of the other two methods. ## Validating the Init Segment ```typescript import { validateC2paInitSegment } from '@svta/cml-c2pa' const initResponse = await fetch(initUrl) const initBytes = new Uint8Array(await initResponse.arrayBuffer()) const init = await validateC2paInitSegment(initBytes) if (init.isValid && init.merkleMaps.length > 0) { // VOD Merkle stream: proceed to validate media segments against init.merkleMaps } ``` `merkleMaps` is not empty only when the `c2pa.hash.bmff.v3` assertion of the init segment has a `merkle` field. Check `isValid` before you use `merkleMaps`. The init manifest must carry a claim signature that verifies. An init segment without a `c2pa.signature` box fails with `C2paStatusCode.CLAIM_SIGNATURE_MISSING`, and its `merkleMaps` must not be trusted. The signature is verified with the certificate inside the manifest. The library does not check that certificate against a trust list. See [Signer Trust](https://cml.svta.org/llms/c2pa/docs/results-and-error-codes.md#signer-trust). If the `exclusions` of the `c2pa.hash.bmff.v3` assertion exclude the whole `moov` box, the init segment fails with `LiveVideoStatusCode.INIT_INVALID`. The library applies this rule of C2PA section 19.2.3 to VOD Merkle init segments too. A nested exclusion such as `/moov/udta` keeps the init segment valid. A `subset` exclusion of the `moov` box also keeps the init segment valid, unless the ranges cover the whole box. ## Validating Media Segments ```typescript import { validateC2paMerkleSegment } from '@svta/cml-c2pa' import type { MerkleSegmentState } from '@svta/cml-c2pa' let state: MerkleSegmentState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const { result, nextState } = await validateC2paMerkleSegment(bytes, init.merkleMaps, state) state = nextState if (result.isValid) { console.log(`Location ${result.location}: valid`) } else { console.error(`Location ${result.location}: failed`, result.errorCodes) } } ``` Call this function only after `validateC2paInitSegment` has confirmed VOD Merkle mode, so `merkleMaps` is not empty. > [!NOTE] > Reset `state` after a seek by passing `undefined`. The first location seen for each track is always accepted, because there is no earlier state to compare with. ## Complete Example ```typescript import { validateC2paInitSegment, validateC2paMerkleSegment, LiveVideoStatusCode } from '@svta/cml-c2pa' import type { MerkleSegmentState } from '@svta/cml-c2pa' async function validateStream(initUrl: string, segmentUrls: string[]) { const initResponse = await fetch(initUrl) const initBytes = new Uint8Array(await initResponse.arrayBuffer()) const init = await validateC2paInitSegment(initBytes) if (!init.isValid) { console.error('Init segment failed', init.errorCodes) return } if (init.merkleMaps.length === 0) { console.error('Not a VOD Merkle stream') return } let state: MerkleSegmentState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const { result, nextState } = await validateC2paMerkleSegment(bytes, init.merkleMaps, state) state = nextState if (result.isValid) { console.log(`Location ${result.location}: valid`) } else { for (const code of result.errorCodes) { switch (code) { case LiveVideoStatusCode.ASSERTION_INVALID: console.error(`Location ${result.location}: discontinuity`) break default: console.error(`Location ${result.location}: failed`, code) } } } } } ``` ## Location Continuity Per §15.12.2, the location values of a Merkle tree start at zero and increase by one for each following chunk. `MerkleSegmentState.lastLocations` tracks the last seen location per track, keyed by `${uniqueId}:${localId}`. The baseline advances on any segment whose Merkle proof verifies, even at an unexpected location. A dropped or reordered segment is still flagged with `LiveVideoStatusCode.ASSERTION_INVALID`, but a later segment that resumes in order validates without an error. A segment whose proof fails does not advance the baseline. ## Result Fields The `MerkleSegmentValidation` result contains: | Field | Type | Description | |-------|------|-------------| | `location` | `number \| null` | This segment's leaf index, or `null` if no merkle box was found | | `bmffHashHex` | `string \| null` | Hex-encoded leaf hash | | `isValid` | `boolean` | All checks passed | | `errorCodes` | `readonly (LiveVideoStatusCode \| C2paStatusCode)[]` | Failure codes (empty when valid) | A `MerkleMap`, one per track, is returned in `InitSegmentValidation.merkleMaps` and contains: | Field | Type | Description | |-------|------|-------------| | `uniqueId` | `number` | Distinguishes tracks that share a `localId` | | `localId` | `number` | Identifies the Merkle tree for this track | | `count` | `number` | Number of leaf nodes in the tree | | `hashes` | `readonly Uint8Array[]` | The tree row stored in the manifest | | `initHash` | `Uint8Array \| null` | Hash binding the init segment's own bytes | | `alg` | `string \| null` | Hash algorithm | | `exclusions` | `readonly BmffHashExclusion[]` | Byte ranges excluded from hashing | | `offsetPrefixSize` | `number` | Leaf hash offset-prefix size (0 or 8 bytes) | > [!NOTE] > `MerkleSegmentValidation.errorCodes` mixes two code sets. `C2paStatusCode` values (`assertion.bmffHash.malformed` and `assertion.bmffHash.mismatch`) report the Merkle proof. `LiveVideoStatusCode.ASSERTION_INVALID` reports location continuity. A failed `initHash` binding is reported on the init segment instead: `InitSegmentValidation.errorCodes` gets both `livevideo.init.invalid` and `assertion.bmffHash.mismatch`. --- title: Results and Error Codes description: Interpret validation results, error codes, and manifest data --- # Results and Error Codes All validation functions in `@svta/cml-c2pa` return structured results with an `isValid` boolean and an `errorCodes` array. This guide explains the results, the error codes, and the manifest data. ## Checking Validation Results Every result follows the same pattern: ```typescript if (!result.isValid) { for (const code of result.errorCodes) { console.error(`Validation failure: ${code}`) } } ``` The `errorCodes` array may contain several codes when several checks fail. When `isValid` is `true`, the array is empty. ## Signer Trust `isValid` is `true` when the manifest is well formed and the claim signature verifies. Every store assertion must be referenced by the claim and match its hash. The manifest must have exactly one hard binding. The signature is verified with the end-entity certificate that the manifest carries in its `x5chain` header. The library does not check that certificate against a trust list. Any party with a certificate can produce a manifest that validates. Before you present content as authentic, compare the signer with your own trust anchors. `InitSegmentValidation.certificate` and `ManifestBoxValidationResult.certificate` hold the DER-encoded end-entity certificate from the claim signature. The value is `null` when the signature is absent or carries no certificate. A manifest without a `c2pa.signature` box, or with a claim `signature` URI that does not resolve to that box, fails with `C2paStatusCode.CLAIM_SIGNATURE_MISSING`. A signature that carries no certificate, or that does not verify over the claim, fails with `C2paStatusCode.CLAIM_SIGNATURE_MISMATCH`. A claim box whose CBOR does not decode fails with `C2paStatusCode.CLAIM_CBOR_INVALID`. A claim box whose CBOR is not a map, for example a map inside a CBOR tag, fails with `C2paStatusCode.CLAIM_MALFORMED`. The library does not check the fields of the claim. An assertion whose CBOR does not decode fails with `C2paStatusCode.ASSERTION_CBOR_INVALID` (C2PA section 15.10.3.1). An assertion whose JSON is not UTF-8 or does not parse fails with `C2paStatusCode.ASSERTION_JSON_INVALID`. These checks apply to every assertion, whatever the label. CBOR that is not deterministic still passes, for example a map with unsorted keys. The CBOR decoder also rejects some CBOR that is well-formed. An assertion with such CBOR reports `ASSERTION_CBOR_INVALID`, and a claim with such CBOR reports `CLAIM_CBOR_INVALID`. The rejected forms are: - a map key that is an array, a map, a byte string, or a tag - a map with a duplicate key, where keys with the same name as a string count as duplicates, for example `1` and `"1"` - an unassigned simple value - a tag number above 2^53 - nesting deeper than 128 levels ## Live Video Error Codes The `LiveVideoStatusCode` constants are the live video validation failures that C2PA specification section 19.7 defines. ```typescript import { LiveVideoStatusCode } from '@svta/cml-c2pa' ``` | Constant | Value | Meaning | |----------|-------|---------| | `INIT_INVALID` | `livevideo.init.invalid` | Init segment contains an `mdat` box. Init manifest without a `c2pa.hash.bmff.v3` assertion. Init content hash that does not include the `moov` box, or that does not match (with `ASSERTION_BMFFHASH_MISMATCH`). | | `MANIFEST_INVALID` | `livevideo.manifest.invalid` | C2PA Manifest Box failed standard validation | | `SEGMENT_INVALID` | `livevideo.segment.invalid` | Signature, hash, or key failure. Key outside the validity period. Manifest box segment without a `c2pa.hash.bmff.v3` assertion. VSI sequence number not moving forward or below the key minimum. Broken `c2pa.manifestId` chain. Manifest box claim `signature` URI that does not name the manifest label. Failed custom continuity validator. | | `ASSERTION_INVALID` | `livevideo.assertion.invalid` | Manifest box segment: sequenceNumber or streamId mismatch | | `CONTINUITY_METHOD_INVALID` | `livevideo.continuityMethod.invalid` | `continuityMethod` absent, unsupported, or required companion fields missing | | `CONTINUITY_METHOD_UNSUPPORTED` | `livevideo.continuityMethod.unsupported` | Custom continuity method with no registered validator (always alongside `continuityMethod.invalid`) | | `SESSIONKEY_INVALID` | `livevideo.sessionkey.invalid` | A session key in the init segment is invalid, or the `c2pa.session-keys` assertion has no session key or does not decode | Versions 1.3.0 and earlier report `SESSIONKEY_INVALID` from `validateC2paSegment` for a session key that expired after init segment validation. That code does not match C2PA section 19.7.3. To check the validity period of a session key, see [Session Key Lifecycle](https://cml.svta.org/llms/c2pa/docs/vsi-validation.md#session-key-lifecycle). Example of handling specific error codes: ```typescript import { LiveVideoStatusCode } from '@svta/cml-c2pa' for (const code of result.errorCodes) { switch (code) { case LiveVideoStatusCode.SEGMENT_INVALID: // Signature, hash, key, or VSI sequence failure break case LiveVideoStatusCode.SESSIONKEY_INVALID: // Init segment has an invalid session key break case LiveVideoStatusCode.ASSERTION_INVALID: // Sequence number or stream ID problem (manifest box method) break case LiveVideoStatusCode.CONTINUITY_METHOD_INVALID: // Manifest ID chain is broken break case LiveVideoStatusCode.INIT_INVALID: // Init segment structure is invalid break case LiveVideoStatusCode.MANIFEST_INVALID: // Embedded manifest failed validation break } } ``` ### Invalid Session Keys `validateC2paInitSegment` reports `SESSIONKEY_INVALID` if it finds an invalid session key in the `c2pa.session-keys` assertion (C2PA section 19.7.3). A session key is invalid if it does not conform to section 18.25.2, or if its signer binding fails verification. The library does not check every rule of section 18.25.2. `sessionKeys` contains only the session keys that pass these checks. The COSE key must include a `kid` (label 2). `minSequenceNumber` and `validityPeriod` must be unsigned integers up to `Number.MAX_SAFE_INTEGER` (2^53 - 1). `createdAt` must be CBOR tag 0 with an RFC 3339 date-time string (RFC 8949 section 3.4.1). The letters `T` and `Z` must be uppercase. The date must exist, so `2025-02-30T00:00:00Z` is invalid. A leap second (second 60) is also invalid, because a JavaScript `Date` cannot represent it. An expired session key does not count as invalid. `sessionKeys` excludes the expired key, and the result has no error code for that key. The assertion must contain at least one session key (section 18.25.2). `validateC2paInitSegment` reports `SESSIONKEY_INVALID` if the assertion has no session key, or if its CBOR does not decode. This rule also applies in VOD Merkle mode. If the CBOR does not decode, the result also includes `C2paStatusCode.ASSERTION_CBOR_INVALID`. Versions 1.3.0 and earlier report no error code for an invalid session key if another session key is valid. These versions also accept a `kid` outside the COSE key. They throw an error if the library cannot verify a session key, for example a key type that it does not support. They also throw an error if the CBOR of the `c2pa.session-keys` assertion does not decode. If your code catches these errors, check `errorCodes` instead. ## C2PA Status Codes The `C2paStatusCode` constants represent manifest integrity failures defined in C2PA specification sections 15 and 18. ```typescript import { C2paStatusCode } from '@svta/cml-c2pa' ``` | Constant | Value | Meaning | |----------|-------|---------| | `ASSERTION_HASHEDURI_MISMATCH` | `assertion.hashedURI.mismatch` | Assertion hash does not match the claim reference | | `ASSERTION_MISSING` | `assertion.missing` | The store has no box for a claim reference, or more than one box for its label | | `ASSERTION_MULTIPLE_HARD_BINDINGS` | `assertion.multipleHardBindings` | The manifest has more than one hard binding assertion | | `ASSERTION_UNDECLARED` | `assertion.undeclared` | The assertion store holds an assertion that the claim does not reference | | `ASSERTION_CBOR_INVALID` | `assertion.cbor.invalid` | An assertion box holds CBOR that does not decode | | `ASSERTION_JSON_INVALID` | `assertion.json.invalid` | An assertion box holds JSON that is not UTF-8 or does not parse | | `ASSERTION_ACTION_INGREDIENT_MISMATCH` | `assertion.action.ingredientMismatch` | Action requires an ingredient reference but none is present | | `CLAIM_SIGNATURE_MISMATCH` | `claimSignature.mismatch` | Claim signature verification failed, or the signature carries no certificate | | `CLAIM_SIGNATURE_MISSING` | `claimSignature.missing` | The manifest has no `c2pa.signature` box, or the claim `signature` URI does not resolve to it | | `CLAIM_MISSING` | `claim.missing` | The manifest has no claim box | | `CLAIM_CBOR_INVALID` | `claim.cbor.invalid` | The claim box holds CBOR that does not decode | | `CLAIM_MALFORMED` | `claim.malformed` | The claim box holds CBOR that is not a map | | `CLAIM_MULTIPLE` | `claim.multiple` | The manifest has more than one claim box | | `CLAIM_HARD_BINDINGS_MISSING` | `claim.hardBindings.missing` | The manifest has no hard binding assertion | | `ASSERTION_BMFFHASH_MALFORMED` | `assertion.bmffHash.malformed` | BMFF hash assertion or Merkle structure is malformed | | `ASSERTION_BMFFHASH_MISMATCH` | `assertion.bmffHash.mismatch` | BMFF content hash does not match the committed value | Versions 1.2.0 and earlier report `claim.signature.mismatch` for `CLAIM_SIGNATURE_MISMATCH`. That value does not match the C2PA specification. Compare codes with the `C2paStatusCode` constants, not with string literals. > [!NOTE] > `C2paStatusCode` values appear in `InitSegmentValidation.errorCodes` and `ManifestBoxValidationResult.errorCodes`, which check manifest integrity. They do not appear in `SegmentValidationResult.errorCodes`: the VSI/EMSG segment validation (Verifiable Segment Info, in event message boxes) uses only `LiveVideoStatusCode`. > > For VOD Merkle streams, `InitSegmentValidation` extracts `merkleMaps` from the `c2pa.hash.bmff.v3` assertion and validates the `initHash` binding of each entry. VOD Merkle segments do not use session keys. An invalid session key still causes `SESSIONKEY_INVALID`. See [Invalid Session Keys](#invalid-session-keys). ## Working with Manifest Data The `C2paManifest` type represents a parsed C2PA manifest. It is available through `InitSegmentValidation.manifest` (VSI method) and `ManifestBoxValidationResult.manifest` (Manifest Box method). | Field | Type | Description | |-------|------|-------------| | `label` | `string` | Manifest label (identifier within the manifest store) | | `instanceId` | `string \| null` | Unique instance ID | | `claimGenerator` | `string \| null` | Tool that generated the claim | | `signatureInfo` | `C2paSignatureInfo` | Signature metadata | | `assertions` | `readonly C2paAssertion[]` | All assertions in the assertion store. Read them only when `isValid` is `true`. | The `signatureInfo` object contains: | Field | Type | Description | |-------|------|-------------| | `issuer` | `string \| null` | Certificate issuer (Common Name or Organization) | | `certNotBefore` | `string \| null` | Certificate validity start (ISO 8601) | ```typescript const { isValid, manifest } = init // or result for the Manifest Box method // The assertions list is the raw assertion store, so read it only when isValid is true. if (isValid && manifest) { console.log('Label:', manifest.label) console.log('Instance ID:', manifest.instanceId) console.log('Claim generator:', manifest.claimGenerator) console.log('Issuer:', manifest.signatureInfo.issuer) console.log('Cert not before:', manifest.signatureInfo.certNotBefore) for (const assertion of manifest.assertions) { console.log(`Assertion [${assertion.label}]:`, assertion.data) } } ``` `data` is the decoded content of the assertion. For a CBOR assertion, a map is a plain object with string keys, a byte string is a `Uint8Array`, and an unsigned integer up to `Number.MAX_SAFE_INTEGER` (2^53 - 1) is a number. A larger integer is a BigInt. CBOR tag 0 and tag 1 are a `Date`. Every other tag is a `{ tag, value }` object. For a JSON assertion, `data` is the parsed JSON. For other content, `data` is the raw bytes of the content box. `data` is also the raw bytes for CBOR that does not decode or JSON that does not parse. ## Sequence Validation Reasons With the [VSI/EMSG method](https://cml.svta.org/llms/c2pa/docs/vsi-validation.md), each `SegmentValidationResult` includes a `sequenceResult` field: a discriminated union on `reason` with `SequenceValidationReason` constants: ```typescript import { SequenceValidationReason } from '@svta/cml-c2pa' ``` | Constant | Value | Valid | Additional Fields | Description | |----------|-------|-------|-------------------|-------------| | `VALID` | `valid` | `true` | none | Sequence number is the next expected value | | `DUPLICATE` | `duplicate` | `false` | none | Sequence number was already seen | | `GAP_DETECTED` | `gap_detected` | `false` | `missingFrom`, `missingTo` | One or more sequence numbers were skipped. The segment is not invalid for this reason. | | `OUT_OF_ORDER` | `out_of_order` | `false` | none | Sequence number is less than the last seen | | `SEQUENCE_NUMBER_BELOW_MINIMUM` | `sequence_number_below_minimum` | `false` | none | Below the session key's `minSequenceNumber` | Narrow the type to access the `GAP_DETECTED` fields: ```typescript import { SequenceValidationReason } from '@svta/cml-c2pa' if (result.sequenceResult.reason === SequenceValidationReason.GAP_DETECTED) { console.warn( `Missing segments ${result.sequenceResult.missingFrom}` + ` through ${result.sequenceResult.missingTo}`, ) } ``` ## Choosing a Validation Method The two validation methods serve different use cases: | Aspect | VSI/EMSG (section 19.4) | Manifest Box (section 19.3) | |--------|-------------------------|---------------------------| | Init segment required | Yes | No | | Per-segment overhead | Low (EMSG box only) | High (full manifest per segment) | | Session keys | Yes | No | | Manifest access | Init segment only | Every segment | | Continuity mechanism | Sequence numbers | Manifest ID chaining | | Functions | `validateC2paInitSegment` + `validateC2paSegment` | `validateC2paManifestBoxSegment` | The VSI/EMSG method suits low-overhead real-time streaming where bandwidth matters. The Manifest Box method gives self-contained segments that are verifiable without earlier context. ## References - [C2PA Specification v2.3](https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html) - [C2PA Live Video Validation (section 19.7)](https://c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_live_video_validation_process) - [COSE, RFC 9052](https://www.rfc-editor.org/rfc/rfc9052) --- title: VSI/EMSG Validation description: Validate live streams using the Verifiable Segment Info method (C2PA section 19.4) --- # VSI/EMSG Validation The Verifiable Segment Info (VSI) method is a two-phase approach to C2PA live video validation, defined in section 19.4 of the C2PA specification. The init segment contains the full C2PA manifest and the session keys. Each media segment contains a small EMSG box with a COSE_Sign1 signature, verified against those session keys. ## Overview The validation flow has two phases: 1. **Init segment**: call `validateC2paInitSegment` once. It parses the C2PA manifest, verifies its integrity, and extracts the validated session keys. 2. **Media segments**: call `validateC2paSegment` for each media segment. The function finds the C2PA EMSG box and verifies the COSE_Sign1 signature against a session key. It then checks the BMFF content hash and validates the sequence number. A `SequenceState` object passes from each media segment call to the next and tracks the sequence numbers. ## Step 1: Validate the Init Segment ```typescript import { validateC2paInitSegment } from '@svta/cml-c2pa' const initResponse = await fetch(initUrl) const initBytes = new Uint8Array(await initResponse.arrayBuffer()) const init = await validateC2paInitSegment(initBytes) if (!init.isValid) { throw new Error(`Init segment validation failed: ${init.errorCodes.join(', ')}`) } ``` The returned `InitSegmentValidation` object contains: | Field | Type | Description | |-------|------|-------------| | `manifest` | `C2paManifest \| null` | Parsed manifest (label, assertions, signature info) | | `certificate` | `Uint8Array \| null` | DER-encoded end-entity certificate | | `manifestId` | `string \| null` | Manifest identifier (`urn:c2pa:` URN), from the JUMBF label of the manifest. `null` if the claim `signature` URI does not name the label, as with a URI relative to the manifest. Compare it with the `manifestId` of each segment. | | `sessionKeys` | `readonly ValidatedSessionKey[]` | Session keys with valid signer binding | | `isValid` | `boolean` | All checks passed | | `errorCodes` | `readonly (LiveVideoStatusCode \| C2paStatusCode)[]` | Failure codes (empty when valid) | C2PA section 19.2.3 requires a `c2pa.hash.bmff.v3` assertion in the init manifest. The content hash of that assertion must include the `moov` box. If the init segment does not conform, the result has `LiveVideoStatusCode.INIT_INVALID` and no `sessionKeys`. ### Accessing Session Keys `sessionKeys` includes only keys with a valid signer binding and an unexpired validity period: ```typescript for (const key of init.sessionKeys) { console.log('Key ID:', key.kid) console.log('Curve:', key.jwk.crv) console.log('Min sequence number:', key.minSequenceNumber) console.log('Validity period (seconds):', key.validityPeriod) console.log('Created at:', key.createdAt) } ``` If a session key is invalid, the result includes `LiveVideoStatusCode.SESSIONKEY_INVALID`. See [Invalid Session Keys](https://cml.svta.org/llms/c2pa/docs/results-and-error-codes.md#invalid-session-keys). Each `ValidatedSessionKey` contains: | Field | Type | Description | |-------|------|-------------| | `kid` | `string` | Key ID (hex-encoded) | | `jwk` | `CoseKeyJwk` | Public key in JWK format (WebCrypto-compatible) | | `minSequenceNumber` | `number` | Minimum accepted sequence number | | `validityPeriod` | `number` | Key lifetime in seconds | | `createdAt` | `string` | ISO 8601 timestamp of key creation | ## Step 2: Validate Media Segments ```typescript import { validateC2paSegment } from '@svta/cml-c2pa' import type { SequenceState } from '@svta/cml-c2pa' // undefined on first call — the function creates a fresh state internally let sequenceState: SequenceState | undefined for (const segmentUrl of segmentUrls) { const segmentResponse = await fetch(segmentUrl) const segmentBytes = new Uint8Array(await segmentResponse.arrayBuffer()) const validated = await validateC2paSegment(segmentBytes, init.sessionKeys, sequenceState) if (!validated) { // No C2PA EMSG box in this segment — skip continue } const { result, nextSequenceState } = validated sequenceState = nextSequenceState if (result.isValid) { console.log(`Segment ${result.sequenceNumber}: valid`) } else { console.error(`Segment ${result.sequenceNumber}: failed`, result.errorCodes) } } ``` `validateC2paSegment` returns `null` when the segment does not contain a C2PA EMSG box. Otherwise it returns an object with: - `result`: a `SegmentValidationResult` with the validation outcome - `nextSequenceState`: the sequence state to pass into the next call The `SegmentValidationResult` contains: | Field | Type | Description | |-------|------|-------------| | `sequenceNumber` | `number` | Segment sequence number from the VSI map | | `manifestId` | `string` | Manifest ID referenced by this segment | | `bmffHashHex` | `string \| null` | Hex-encoded BMFF content hash | | `kidHex` | `string \| null` | Hex-encoded key ID used for verification | | `sequenceResult` | `SequenceValidationResult` | Sequence number validation outcome | | `isValid` | `boolean` | All checks passed. A gap does not fail the segment. | | `errorCodes` | `readonly LiveVideoStatusCode[]` | Failure codes (empty when valid) | > [!NOTE] > Always keep `nextSequenceState` and pass it to the next `validateC2paSegment` call. It enables the detection of duplicates, gaps, and out-of-order segments. A duplicate, out-of-order, or below-minimum sequence number fails the segment with `LiveVideoStatusCode.SEGMENT_INVALID`. C2PA 2.4 section 19.7.3 defines only the below-minimum check. The duplicate and out-of-order check is a library rule with the code of the C2PA 2.5 draft. A gap does not fail the segment. `sequenceResult` reports the gap and its missing range. A segment whose signature or content hash does not verify leaves `nextSequenceState` equal to the `sequenceState` that the function received. The same applies to a segment whose session key is missing, not yet active, or expired. After a seek or an encoder restart, pass `undefined` to start a new state. ## Complete Example ```typescript import { validateC2paInitSegment, validateC2paSegment, SequenceValidationReason, } from '@svta/cml-c2pa' import type { SequenceState } from '@svta/cml-c2pa' async function validateStream(initUrl: string, segmentUrls: string[]) { // Phase 1: Validate the init segment const initResponse = await fetch(initUrl) const initBytes = new Uint8Array(await initResponse.arrayBuffer()) const init = await validateC2paInitSegment(initBytes) if (!init.isValid) { console.error('Init segment invalid:', init.errorCodes) return } console.log('Manifest:', init.manifest?.label) console.log('Issuer:', init.manifest?.signatureInfo.issuer) console.log('Session keys:', init.sessionKeys.length) // Phase 2: Validate each media segment // undefined on first call — the function creates a fresh state internally let sequenceState: SequenceState | undefined for (const segmentUrl of segmentUrls) { const response = await fetch(segmentUrl) const bytes = new Uint8Array(await response.arrayBuffer()) const validated = await validateC2paSegment(bytes, init.sessionKeys, sequenceState) if (!validated) { console.log('No C2PA data in segment, skipping') continue } const { result, nextSequenceState } = validated sequenceState = nextSequenceState if (result.isValid) { console.log(`Segment ${result.sequenceNumber}: valid (key: ${result.kidHex})`) } else { console.error(`Segment ${result.sequenceNumber}: failed`, result.errorCodes) } // A gap does not fail the segment. Report the missing segments before a valid segment. if (result.isValid && result.sequenceResult.reason === SequenceValidationReason.GAP_DETECTED) { console.warn( `Missing segments ${result.sequenceResult.missingFrom}` + ` through ${result.sequenceResult.missingTo}`, ) } } } ``` ## Session Key Lifecycle Session keys are extracted from the `c2pa.session-keys` assertion in the init segment manifest. Each key passes signer binding verification before it is included in the validation result. The signer binding verification accepts two forms of the signed payload: the end-entity certificate itself, or the certificate as a CBOR byte string. The text of C2PA section 18.25.2 allows both forms. The signer binding is a `COSE_Sign1` structure with CBOR tag 18 and a payload field. The payload field can be nil, an empty byte string, or an exact copy of the signed payload. If the payload field has other content, the signer binding verification fails. The validation function handles key matching and the validity period: 1. **Key matching**: `validateC2paSegment` matches the `kid` (key ID) from the COSE_Sign1 header against the available session keys. 2. **Validity period**: A key is active from `createdAt` until `createdAt + validityPeriod` (C2PA section 18.25.2). If the matched key is not yet active or has expired, the result includes `LiveVideoStatusCode.SEGMENT_INVALID` (C2PA section 19.7.3). 3. **No match**: If no session key matches the `kid`, the result includes `LiveVideoStatusCode.SEGMENT_INVALID`. `SEGMENT_INVALID` is also the code for signature and hash failures. If you need to know whether the validity period caused the failure, check the matched key: ```typescript import type { SegmentValidationResult, ValidatedSessionKey } from '@svta/cml-c2pa' function isOutsideValidityPeriod( result: SegmentValidationResult, sessionKeys: readonly ValidatedSessionKey[], ): boolean { const key = sessionKeys.find(k => k.kid === result.kidHex) if (!key) return false const now = Date.now() const activeFrom = Date.parse(key.createdAt) const activeUntil = activeFrom + key.validityPeriod * 1000 return now < activeFrom || now > activeUntil } ``` > [!NOTE] > An init segment can contain a session key that becomes active later. `sessionKeys` includes that key. You do not need to validate the same init segment again when the key becomes active. > > When a session key expires during the stream, the signer is expected to produce a new init segment with new session keys. Validate the new init segment and use its `sessionKeys` for the following media segments. ## Sequence Number Validation Each media segment has an increasing sequence number in its VSI map. The sequence number must be an unsigned integer (C2PA section 19.4.2). The library supports values up to `Number.MAX_SAFE_INTEGER` (2^53 - 1). For any other value, `validateC2paSegment` throws an error. The `sequenceResult` field is a discriminated union on `reason`, with `SequenceValidationReason` constants: ```typescript import { SequenceValidationReason } from '@svta/cml-c2pa' ``` | Constant | Value | Valid | Description | |----------|-------|-------|-------------| | `VALID` | `valid` | `true` | Sequence number is the next expected value | | `DUPLICATE` | `duplicate` | `false` | Sequence number was already seen | | `GAP_DETECTED` | `gap_detected` | `false` | One or more sequence numbers were skipped. The segment is not invalid for this reason. | | `OUT_OF_ORDER` | `out_of_order` | `false` | Sequence number is less than the last seen | | `SEQUENCE_NUMBER_BELOW_MINIMUM` | `sequence_number_below_minimum` | `false` | Below the session key's `minSequenceNumber` | Narrow the type to access additional fields: ```typescript import { SequenceValidationReason } from '@svta/cml-c2pa' const { sequenceResult } = result switch (sequenceResult.reason) { case SequenceValidationReason.VALID: // All good break case SequenceValidationReason.GAP_DETECTED: console.warn( `Gap detected: missing segments ${sequenceResult.missingFrom}` + ` to ${sequenceResult.missingTo}`, ) break case SequenceValidationReason.DUPLICATE: case SequenceValidationReason.OUT_OF_ORDER: case SequenceValidationReason.SEQUENCE_NUMBER_BELOW_MINIMUM: console.warn(`Sequence issue: ${sequenceResult.reason}`) break } ``` > [!NOTE] > The sequence state keeps a bounded sliding window of the last 32 sequence numbers, so memory use is constant whatever the stream length. ## API Report File for "@svta/cml-c2pa" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ValueOf } from '@svta/cml-utils'; // @public export type BmffHashConstraint = { readonly offset: number; readonly value: Uint8Array | readonly number[]; }; // @public export type BmffHashExclusion = { readonly xpath: string; readonly length?: number; readonly data?: readonly BmffHashConstraint[]; readonly subset?: readonly BmffHashSubset[]; readonly version?: number; readonly flags?: Uint8Array | readonly number[]; readonly exact?: boolean; }; // @public export type BmffHashSubset = { readonly offset: number; readonly length: number; }; // @public export type C2paAssertion = { readonly label: string; readonly data: unknown; }; // @public export type C2paManifest = { readonly label: string; readonly instanceId: string | null; readonly claimGenerator: string | null; readonly signatureInfo: C2paSignatureInfo; readonly assertions: readonly C2paAssertion[]; }; // @public export type C2paSignatureInfo = { readonly issuer: string | null; readonly certNotBefore: string | null; }; // @public export const C2paStatusCode: { readonly ASSERTION_HASHEDURI_MISMATCH: "assertion.hashedURI.mismatch"; readonly ASSERTION_MISSING: "assertion.missing"; readonly ASSERTION_MULTIPLE_HARD_BINDINGS: "assertion.multipleHardBindings"; readonly ASSERTION_UNDECLARED: "assertion.undeclared"; readonly ASSERTION_CBOR_INVALID: "assertion.cbor.invalid"; readonly ASSERTION_JSON_INVALID: "assertion.json.invalid"; readonly ASSERTION_ACTION_INGREDIENT_MISMATCH: "assertion.action.ingredientMismatch"; readonly CLAIM_SIGNATURE_MISMATCH: "claimSignature.mismatch"; readonly CLAIM_SIGNATURE_MISSING: "claimSignature.missing"; readonly CLAIM_MISSING: "claim.missing"; readonly CLAIM_CBOR_INVALID: "claim.cbor.invalid"; readonly CLAIM_MALFORMED: "claim.malformed"; readonly CLAIM_MULTIPLE: "claim.multiple"; readonly CLAIM_HARD_BINDINGS_MISSING: "claim.hardBindings.missing"; readonly ASSERTION_BMFFHASH_MALFORMED: "assertion.bmffHash.malformed"; readonly ASSERTION_BMFFHASH_MISMATCH: "assertion.bmffHash.mismatch"; }; // @public export type C2paStatusCode = ValueOf; // @public export type CoseKeyJwk = { readonly kty: string; readonly crv: string; readonly x: string; readonly y?: string; }; // @public export type InitSegmentValidation = { readonly manifest: C2paManifest | null; readonly certificate: Uint8Array | null; readonly manifestId: string | null; readonly sessionKeys: readonly ValidatedSessionKey[]; readonly merkleMaps: readonly MerkleMap[]; readonly isValid: boolean; readonly errorCodes: readonly (LiveVideoStatusCode | C2paStatusCode)[]; }; // @public export const LiveVideoStatusCode: { readonly INIT_INVALID: "livevideo.init.invalid"; readonly MANIFEST_INVALID: "livevideo.manifest.invalid"; readonly SEGMENT_INVALID: "livevideo.segment.invalid"; readonly ASSERTION_INVALID: "livevideo.assertion.invalid"; readonly CONTINUITY_METHOD_INVALID: "livevideo.continuityMethod.invalid"; readonly CONTINUITY_METHOD_UNSUPPORTED: "livevideo.continuityMethod.unsupported"; readonly SESSIONKEY_INVALID: "livevideo.sessionkey.invalid"; }; // @public export type LiveVideoStatusCode = ValueOf; // @public export type ManifestBoxContinuityValidator = (liveVideoAssertion: Readonly>, manifest: C2paManifest) => boolean | Promise; // @public export type ManifestBoxValidationOptions = { readonly continuityValidator?: { readonly method: string; readonly validate: ManifestBoxContinuityValidator; }; }; // @public export type ManifestBoxValidationResult = { readonly manifest: C2paManifest | null; readonly issuer: string | null; readonly certificate: Uint8Array | null; readonly sequenceNumber: number | null; readonly previousManifestId: string | null; readonly streamId: string | null; readonly continuityMethod: string | null; readonly bmffHashHex: string | null; readonly isValid: boolean; readonly errorCodes: readonly (LiveVideoStatusCode | C2paStatusCode)[]; }; // @public export type ManifestBoxValidationState = { readonly lastStreamId?: string | null; readonly lastSequenceNumber?: number | null; }; // @public export type MerkleMap = { readonly uniqueId: number; readonly localId: number; readonly count: number; readonly hashes: readonly Uint8Array[]; readonly initHash: Uint8Array | null; readonly alg: string | null; readonly exclusions: readonly BmffHashExclusion[]; readonly offsetPrefixSize: number; }; // @public export type MerkleSegmentState = { readonly lastLocations: ReadonlyMap; }; // @public export type MerkleSegmentValidation = { readonly location: number | null; readonly bmffHashHex: string | null; readonly isValid: boolean; readonly errorCodes: readonly (LiveVideoStatusCode | C2paStatusCode)[]; }; // @public export type SegmentValidationResult = { readonly sequenceNumber: number; readonly manifestId: string; readonly bmffHashHex: string | null; readonly kidHex: string | null; readonly sequenceResult: SequenceValidationResult; readonly isValid: boolean; readonly errorCodes: readonly LiveVideoStatusCode[]; }; // @public export type SequenceState = { readonly lastSequenceNumber: number | null; readonly seenSequences: ReadonlySet; }; // @public export const SequenceValidationReason: { readonly VALID: "valid"; readonly DUPLICATE: "duplicate"; readonly GAP_DETECTED: "gap_detected"; readonly OUT_OF_ORDER: "out_of_order"; readonly SEQUENCE_NUMBER_BELOW_MINIMUM: "sequence_number_below_minimum"; }; // @public export type SequenceValidationReason = ValueOf; // @public export type SequenceValidationResult = { readonly isValid: true; readonly reason: typeof SequenceValidationReason.VALID; } | { readonly isValid: false; readonly reason: typeof SequenceValidationReason.SEQUENCE_NUMBER_BELOW_MINIMUM | typeof SequenceValidationReason.DUPLICATE | typeof SequenceValidationReason.OUT_OF_ORDER; } | { readonly isValid: false; readonly reason: typeof SequenceValidationReason.GAP_DETECTED; readonly missingFrom: number; readonly missingTo: number; }; // @public export function validateC2paInitSegment(bytes: Uint8Array): Promise; // @public export function validateC2paManifestBoxSegment(bytes: Uint8Array, lastManifestId: string | null, state?: ManifestBoxValidationState, options?: ManifestBoxValidationOptions): Promise<{ readonly result: ManifestBoxValidationResult; readonly nextManifestId: string | null; readonly nextState: ManifestBoxValidationState; }>; // @public export function validateC2paMerkleSegment(segmentBytes: Uint8Array, merkleMaps: readonly MerkleMap[], state?: MerkleSegmentState): Promise<{ readonly result: MerkleSegmentValidation; readonly nextState: MerkleSegmentState; }>; // @public export function validateC2paSegment(segmentBytes: Uint8Array, sessionKeys: readonly ValidatedSessionKey[], sequenceState?: SequenceState): Promise<{ readonly result: SegmentValidationResult; readonly nextSequenceState: SequenceState; } | null>; // @public export type ValidatedSessionKey = { readonly kid: string; readonly jwk: CoseKeyJwk; readonly minSequenceNumber: number; readonly validityPeriod: number; readonly createdAt: string; }; // (No @packageDocumentation comment for this package) ``` # @svta/cml-cmaf-ham A collection of tools for working with Common Media Application Format - Hypothetical Application Model (CMAF-HAM). ## When to use - Parse a DASH MPD into the CMAF-HAM presentation model: `dashToHam` - Parse an HLS multivariant playlist into the CMAF-HAM presentation model: `hlsToHam` - Convert one manifest format to the other through the model: `hamToDash`, `hamToHls` - Collect the tracks of a presentation, a selection set, or a switching set: `getTracksFromPresentation`, `getTracksFromSelectionSet`, `getTracksFromSwitchingSet` - Validate a presentation, a switching set, or a track: `validatePresentation`, `validateSwitchingSet`, `validateTrack` Do not write your own DASH to HLS converter or manifest object model. Open an issue for a manifest feature that the model does not carry. ## Installation ```bash npm i @svta/cml-cmaf-ham ``` ## Usage ```typescript import { dashToHam, hamToHls } from "@svta/cml-cmaf-ham"; import type { ManifestFile } from "@svta/cml-cmaf-ham"; // Convert a DASH MPD to HLS playlists through the HAM model. function dashToHls(mpd: string): ManifestFile { const presentations = dashToHam(mpd); return hamToHls(presentations); } ``` ## API Report File for "@svta/cml-cmaf-ham" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { FAIRPLAY_KEY_SYSTEM } from '@svta/cml-drm'; import { PLAYREADY_KEY_SYSTEM } from '@svta/cml-drm'; import { ValueOf } from '@svta/cml-utils'; import { WIDEVINE_KEY_SYSTEM } from '@svta/cml-drm'; // @alpha export type AdaptationSet = { $: { audioSamplingRate?: string; codecs?: string; contentType?: string; frameRate?: string; group?: string; id?: string; lang?: string; maxBandwidth?: string; maxFrameRate?: string; maxHeight?: string; maxWidth?: string; mimeType?: string; minBandwidth?: string; par?: string; sar?: string; segmentAlignment: string; startWithSAP?: string; subsegmentAlignment?: string; subsegmentStartsWithSAP?: string; }; AudioChannelConfiguration?: AudioChannelConfiguration[]; ContentComponent?: ContentComponent[]; Role?: Role[]; Representation: Representation[]; SegmentTemplate?: SegmentTemplate[]; SegmentList?: SegmentList[]; }; // @alpha export type AddressableObject = { url: string; byteRange?: { start: number; end: number; }; }; // @alpha export type AlignedSwitchingSet = { switchingSets: SwitchingSet[]; }; // @alpha export const AUDIO: "audio"; // @alpha export type AudioChannelConfiguration = { $: { schemeIdUri: string; value: string; }; }; // @alpha export type AudioTrack = Track & { sampleRate: number; channels: number; }; // @alpha export type Base = { baseUrls: string[]; }; // @alpha export type Byterange = { length: number; offset: number; }; // @alpha export type ByteRangeObject = { start: number; end: number; }; // @alpha export function byteRangeToDashString(byteRange: ByteRangeObject | undefined): string; // @alpha export function byteRangeToHlsString(byteRange: ByteRangeObject | undefined): string; // @internal export function calculateDuration(duration: string | undefined, timescale: string | undefined): number; // @alpha export const CBCS: "cbcs"; // @alpha export const CENC: "cenc"; // @alpha (undocumented) export type ContentComponent = { $: { contentType: string; id: string; }; }; // @alpha export type DashManifest = { MPD: { $?: { maxSegmentDuration?: string; mediaPresentationDuration?: string; minBufferTime?: string; profiles?: string; type?: string; xmlns?: string; }; Period: Period[]; }; }; // @internal (undocumented) export type DashParser = (raw: string) => DashManifest; // @internal (undocumented) export type DashSerializer = (json: DashManifest) => string; // @alpha export function dashToHam(manifest: string): Presentation[]; // @alpha export function decodeByteRange(byteRange: Byterange | undefined): string; // @alpha export type Duration = { duration: number; }; // @alpha export const DYNAMIC: "dynamic"; // @alpha export function encodeByteRange(track: VideoTrack | AudioTrack): string; // @alpha type Event_2 = { messageData: string; duration: number; id: string; presentationTime: number; adjustedPresentationTime?: number; schemeIdUri: string; }; export { Event_2 as Event } // @alpha export type EventStream = { schemeIdUri: string; timescale: number; events: Event_2[]; presentationTimeOffset: number; }; // @alpha export function formatSegments(segments: SegmentHls[]): Segment[]; // @alpha export type FrameRate = { frameRateNumerator: number; frameRateDenominator?: number; }; // @internal export function getChannels(adaptationSet: AdaptationSet, representation: Representation): number; // @internal export function getCodec(adaptationSet: AdaptationSet, representation: Representation): string; // @internal export function getContentType(adaptationSet: AdaptationSet, representation?: Representation): string; // @internal (undocumented) export function getDashParser(): DashParser; // @internal (undocumented) export function getDashSerializer(): DashSerializer; // @alpha export function getDuration(manifest: HlsManifest, segments: SegmentHls[]): number | null; // @internal export function getFrameRate(adaptationSet: AdaptationSet, representation: Representation): FrameRate; // @internal (undocumented) export function getGroup(adaptationSet: AdaptationSet): string; // @alpha export function getHlsCodec(type: string, codecs?: string): string; // @internal (undocumented) export function getHlsParser(): HlsParser; // @internal export function getLanguage(adaptationSet: AdaptationSet): string; // @internal export function getNumberOfSegments(segmentTemplate: SegmentTemplate, duration: number): number; // @alpha export function getPlaylistData(track: AudioTrack | VideoTrack): string; // @internal export function getPresentationId(period: Period, duration: number): string; // @internal export function getSampleRate(adaptationSet: AdaptationSet, representation: Representation): number; // @internal export function getSar(adaptationSet: AdaptationSet, representation: Representation): string; // @alpha export function getSegments(segments: Segment[]): string; // @internal export function getTimescale(track: Track): number; // @internal export function getTrackDuration(segments: Segment[]): number; // @alpha export function getTracksFromPresentation(presentation: Presentation, predicate?: (track: Track) => boolean): Track[]; // @alpha export function getTracksFromSelectionSet(selectionSet: SelectionSet, predicate?: (track: Track) => boolean): Track[]; // @alpha export function getTracksFromSwitchingSet(switchingSet: SwitchingSet, predicate?: (track: Track) => boolean): Track[]; // @internal export function getUrlFromTemplate(representation: Representation, segmentTemplate: SegmentTemplate, segmentId: number): string; // @alpha export function getUrlInitialization(track: VideoTrack | AudioTrack): string; // @alpha export type Ham = { id: string; }; // @alpha export function hamToDash(presentation: Presentation[]): ManifestFile; // @alpha export function hamToHls(presentation: Presentation[]): ManifestFile; // @alpha export function hlsByterangeToByteRangeObject(byterange: { length: number; offset: number; } | undefined): ByteRangeObject | undefined; // @alpha export type HlsManifest = { playlists: PlayList[]; mediaGroups: MediaGroups; segments: SegmentHls[]; targetDuration?: number; version?: number; mediaSequence?: number; }; // @internal (undocumented) export type HlsParser = (text: string) => HlsManifest; // @alpha export function hlsToHam(manifest: string, ancillaryManifests: string[]): Presentation[]; // @alpha export const IMAGE: "image"; // @alpha export type Initialization = { $: { range?: string; sourceURL?: string; }; }; // @internal (undocumented) export function iso8601DurationToNumber(isoDuration: string): number; // @alpha export type KeySystem = typeof FAIRPLAY_KEY_SYSTEM | typeof PLAYREADY_KEY_SYSTEM | typeof WIDEVINE_KEY_SYSTEM; // @alpha export type KeySystemMap = Record; // @alpha export type ManifestFile = { type: ManifestFormat; manifest: string; fileName?: string; ancillaryManifests?: ManifestFile[]; metadata?: Map; }; // @alpha export type ManifestFormat = "hls" | "dash"; // @alpha export const ManifestType: { readonly DYNAMIC: typeof DYNAMIC; readonly STATIC: typeof STATIC; }; // @alpha export type ManifestType = ValueOf; // @internal export function mapSegmentBase(representation: Representation, duration: number): Segment[]; // @internal export function mapSegmentList(segmentList: SegmentList[]): Segment[]; // @internal export function mapSegments(adaptationSet: AdaptationSet, representation: Representation, duration: number): Segment[]; // @internal export function mapSegmentTemplate(representation: Representation, duration: number, segmentTemplate: SegmentTemplate): Segment[]; // @alpha export type MediaGroups = { AUDIO: Record>; SUBTITLES: Record>; }; // @internal (undocumented) export function numberToIso8601Duration(duration: number): string; // @alpha export function parseByteRangeString(byteRangeString: string | undefined): ByteRangeObject | undefined; // @internal export function parseDashManifest(raw: string): DashManifest; // @internal (undocumented) export function parseHlsManifest(text?: string): HlsManifest; // @alpha export type Period = { $: { duration: string; id?: string; start?: string; }; AdaptationSet: AdaptationSet[]; }; // @alpha export type PlayList = { uri: string; attributes: { FRAME_RATE: number; "FRAME-RATE"?: number; LANGUAGE?: string; CODECS: string; BANDWIDTH: number; RESOLUTION: { width: number; height: number; }; }; }; // @alpha export type Presentation = Ham & Duration & Base & { selectionSets: SelectionSet[]; startTime: number; endTime: number; eventStreams?: EventStream[]; }; // @alpha export type Protection = { defaultKid: string; keySystems: KeySystemMap; scheme: ProtectionScheme; }; // @alpha export const ProtectionScheme: { readonly CENC: typeof CENC; readonly CBCS: typeof CBCS; }; // @alpha export type ProtectionScheme = ValueOf; // @alpha export type Representation = { $: { audioSamplingRate?: string; bandwidth: string; codecs?: string; frameRate?: string; height?: string; id: string; mimeType?: string; sar?: string; scanType?: string; startWithSAP?: string; width?: string; }; AudioChannelConfiguration?: AudioChannelConfiguration[]; BaseURL?: string[]; SegmentBase?: SegmentBase[]; SegmentList?: SegmentList[]; SegmentTemplate?: SegmentTemplate[]; }; // @alpha export type Role = { $: { schemeIdUri: string; value: string; }; }; // @alpha export type Segment = Ham & AddressableObject & Duration & { startTime: number; }; // @alpha export type SegmentBase = { $: { indexRange: string; indexRangeExact: string; timescale: string; }; Initialization: Initialization[]; }; // @alpha export type SegmentHls = { title?: string; duration: number; byterange?: Byterange; uri?: string; timeline?: number; map?: { uri: string; byterange: Byterange; }; }; // @alpha export type SegmentList = { $: { duration: string; timescale: string; }; Initialization: Initialization[]; SegmentURL?: SegmentURL[]; }; // @alpha export type SegmentTemplate = { $: { duration: string; initialization: string; media: string; startNumber: string; timescale: string; }; }; // @alpha export type SegmentURL = { $: { media?: string; }; }; // @alpha export type SelectionSet = Ham & { switchingSets: SwitchingSet[]; alignedSwitchingSets?: AlignedSwitchingSet[]; type: TrackType; }; // @internal (undocumented) export function serializeDashManifest(json: DashManifest): string; // @internal (undocumented) export function setDashParser(parser: DashParser): void; // @internal (undocumented) export function setDashSerializer(serializer: DashSerializer): void; // @internal (undocumented) export function setHlsParser(parser: HlsParser): void; // @alpha export const STATIC: "static"; // @alpha export type SwitchingSet = Ham & Base & { tracks: Track[]; protection?: Protection; role?: string; }; // @alpha export const TEXT: "text"; // @alpha type TextTrack_2 = Track; export { TextTrack_2 as TextTrack } // @alpha export type Track = Ham & Base & Duration & Omit & { url?: string; type: TrackType; codecs: string[]; mimeType: string; language?: string; bandwidth: number; initialization: AddressableObject; segments: Segment[]; segmentIndex?: AddressableObject & { timescale: number; }; presentationTimeOffset?: number; }; // @alpha export const TrackType: { readonly AUDIO: typeof AUDIO; readonly VIDEO: typeof VIDEO; readonly TEXT: typeof TEXT; readonly IMAGE: typeof IMAGE; }; // @alpha export type TrackType = ValueOf; // @alpha export function validatePresentation(presentation: Presentation): Validation; // @alpha export function validateSegment(segment: Segment, trackId?: string, prevValidation?: Validation): Validation; // @alpha export function validateSegments(segments: Segment[], trackId?: string, prevValidation?: Validation): Validation; // @alpha export function validateSelectionSet(selectionSet: SelectionSet, presentationId?: string, prevValidation?: Validation): Validation; // @alpha export function validateSelectionSets(selectionSets: SelectionSet[], presentationId?: string, prevValidation?: Validation): Validation; // @alpha export function validateSwitchingSet(switchingSet: SwitchingSet, selectionSetId?: string, prevValidation?: Validation): Validation; // @alpha export function validateSwitchingSets(switchingSets: SwitchingSet[], selectionSetId?: string, prevValidation?: Validation): Validation; // @alpha export function validateTrack(track: Track, switchingSetId?: string, prevValidation?: Validation): Validation; // @alpha export function validateTracks(tracks: Track[], switchingSetId?: string, prevValidation?: Validation): Validation; // @alpha export type Validation = { status: boolean; errorMessages: string[]; }; // @alpha export const VIDEO: "video"; // @alpha export type VideoTrack = Track & { width: number; height: number; frameRate: FrameRate; par: string; sar: string; scanType: string; }; // (No @packageDocumentation comment for this package) ``` # @svta/cml-cmcd Common Media Client Data (CMCD) encoding and decoding. ## When to use - Add a CMCD report to a segment request as a query string or as headers: `appendCmcdQuery`, `appendCmcdHeaders`, `toCmcdQuery`, `toCmcdHeaders` - Encode or decode CMCD key and value pairs: `encodeCmcd`, `decodeCmcd`, `fromCmcdQuery`, `fromCmcdHeaders` - Report CMCD v2 events and time intervals to a collector: `CmcdReporter`, `createFetchTransport`, `createXhrTransport` - Validate a CMCD payload against CTA-5004: `validateCmcd`, `validateCmcdEventReport` - Capture the CMCD that a player sends in a test: `CmcdReportRecorder` Do not write your own CMCD serializer, key sorter, header grouper, or event reporter. Open an issue for a key or a reporting mode that is missing. ## Installation ```bash npm i @svta/cml-cmcd ``` ## Usage ```typescript import { CmcdReportingMode, encodeCmcd } from "@svta/cml-cmcd"; const input = { br: [1000], "com.example-hello": "world", ec: ["ERR001", "ERR002"], su: true, }; const options = { version: 2 as const, reportingMode: CmcdReportingMode.REQUEST }; const result = encodeCmcd(input, options); // result === 'br=(1000),com.example-hello="world",ec=("ERR001" "ERR002"),su,v=2' ``` ## Testing CMCD output with `CmcdReportRecorder` `CmcdReportRecorder` captures the CMCD requests that a player under test sends, whether through `XMLHttpRequest` or `fetch`. Each capture is normalized to [`HttpRequest`](https://github.com/streaming-video-technology-alliance/common-media-library/tree/main/libs/utils), so tests work the same for both transports. ````ts import { CmcdReportRecorder, validateCmcdRequest } from '@svta/cml-cmcd' const recorder = new CmcdReportRecorder() recorder.attach({ eventTargetUrls: ['https://events.example.com'] }) // ... configure and start the player under test ... const segments = await recorder.waitForSegments({ count: 3 }) for (const r of segments) { const result = validateCmcdRequest(r.request) if (!result.valid) { console.error(result.issues) } } recorder.detach() ```` --- title: Report Recorder Guide description: Capture and assert on CMCD-bearing reports in player e2e tests --- # CMCD Report Recorder Guide `CmcdReportRecorder` is a test helper that captures the HTTP requests with CMCD data that a video player under test sends. It patches `XMLHttpRequest` and `fetch` in the current realm. It normalizes captured reports to one structure, whichever transport produced them. It offers functions for assertions after the run and for live inspection. Use it when you need to: - Verify that a player emits CMCD data correctly during a real playback session - Assert on the number of reports with CMCD data of a given type (manifest, segment, event) - Validate CMCD payloads with `validateCmcdRequest` from the same package - Intercept event-mode POSTs without making real network calls to placeholder endpoints - Show recorded reports live in a test harness page for human inspection The recorder ships in `@svta/cml-cmcd`, so adopters get a tested, documented helper next to the encoder they already use. ## Quick start ```typescript import { CmcdReportRecorder } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach(); // ... configure and start the player under test ... const segments = await recorder.waitForSegments({ count: 3 }); console.log(`Recorded ${segments.length} segment reports with CMCD data`); recorder.detach(); ``` `attach()` installs the transport patches, the player runs and sends requests, the test asserts on the recorded reports, and `detach()` restores the original transports. ## What gets recorded The recorder records a request when it has CMCD data in any of three forms: | Reporting mode | Trigger | | -------------- | ---------------------------------------------------- | | `'query'` | URL contains the `CMCD=...` query parameter | | `'header'` | Request includes any `Cmcd-*` header | | `'event'` | POST to a URL registered in `eventTargetUrls` option | Requests without CMCD data pass through untouched and are not stored. The recorder classifies each report by URL extension and HTTP method: | Type | Heuristic | | ----------------------------- | ---------------------------------------------------------- | | `CmcdRecordedRequestType.MANIFEST` | URL ends in `.m3u8` or `.mpd` | | `CmcdRecordedRequestType.SEGMENT` | URL ends in `.m4s`, `.m4v`, `.m4a`, `.mp4`, `.ts`, `.aac` | | `CmcdRecordedRequestType.EVENT` | `POST` to a URL in `eventTargetUrls` | | `CmcdRecordedRequestType.UNKNOWN` | Anything else carrying CMCD data | Recorded entries have one structure: ```typescript import type { CmcdRecordedReportMode, CmcdRecordedRequestType } from "@svta/cml-cmcd"; import type { HttpRequest } from "@svta/cml-utils"; type CmcdRecordedReport = { readonly request: HttpRequest; // normalized; headers lowercase, body as string readonly type: CmcdRecordedRequestType; readonly reportingMode: CmcdRecordedReportMode; readonly timestamp: number; // Date.now() at capture time }; ``` This structure is what `validateCmcdRequest` from `@svta/cml-cmcd` accepts, so you can pass recorded reports directly to validation. ## Reading recorded reports There are three ways to read recorded reports: ### `getReports()`: snapshot Returns a copy of everything recorded so far, so changes to the result do not affect the recorder. Best for tests that run the player to the end before they assert. Filter the result yourself if you need a subset: ```typescript import { CmcdReportRecorder, CmcdRecordedRequestType } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach(); // ... player runs to completion ... const all = recorder.getReports(); const manifests = all.filter((r) => r.type === CmcdRecordedRequestType.MANIFEST); const segments = all.filter((r) => r.type === CmcdRecordedRequestType.SEGMENT); console.log(`Total: ${all.length}, manifests: ${manifests.length}, segments: ${segments.length}`); recorder.detach(); ``` ### `waitFor*({ count?, timeout? })`: positive assertion Resolves once at least `count` matching reports are recorded. Rejects with a diagnostic error on timeout. Use these methods for "expect N to happen" assertions while the player is still running. There is one method per request type, plus a generic `waitForReports` that matches any type: | Method | Matches | | --------------------------------------- | ------------------------------------ | | `waitForReports(options?)` | any recorded report | | `waitForManifest(options?)` | reports with `type === MANIFEST` | | `waitForSegments(options?)` | reports with `type === SEGMENT` | | `waitForEvents(options?)` | reports with `type === EVENT` (POST) | Each method takes one `CmcdReportRecorderWaitOptions` object with two optional fields. `count` defaults to `1`. `timeout` defaults to the recorder's `waitTimeout` option, which is 15 seconds if unset. ```typescript import { CmcdReportRecorder } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach(); // Kick off playback in the background... startPlayer(); // Wait for the first three segment reports const segments = await recorder.waitForSegments({ count: 3 }); for (const report of segments) { console.log(report.request.url, report.reportingMode); } recorder.detach(); ``` Shorten the timeout per call or for the whole recorder: ```typescript import { CmcdReportRecorder } from "@svta/cml-cmcd"; // Per call: fail in 2 seconds rather than the recorder default await recorder.waitForManifest({ timeout: 2000 }); // All wait* calls on this recorder default to 2 seconds const fastRecorder = new CmcdReportRecorder(); fastRecorder.attach({ waitTimeout: 2000 }); await fastRecorder.waitForManifest(); ``` ## Live inspection with `onReport` For test harness pages that show CMCD activity live, pass an `onReport` callback to `attach()`. The callback fires synchronously for each recorded report, right after the report is appended to the buffer and before any pending `waitFor*` promises resolve. ```typescript import { CmcdReportRecorder, type CmcdRecordedReport, } from "@svta/cml-cmcd"; const tableBody = document.querySelector("#cmcd-log tbody")!; const recorder = new CmcdReportRecorder(); recorder.attach({ onReport: (report: CmcdRecordedReport) => { const row = tableBody.insertRow(); row.insertCell().textContent = new Date(report.timestamp).toISOString(); row.insertCell().textContent = report.type; row.insertCell().textContent = report.reportingMode; row.insertCell().textContent = report.request.url; }, }); // ... player runs; the table updates in real time as reports arrive ... ``` The callback receives the same object reference that `getReports()` returns. So one panel can subscribe to all reports and branch on `report.type` or `report.reportingMode` to send each report to its own view: ```typescript import { CmcdRecordedRequestType } from "@svta/cml-cmcd"; recorder.attach({ onReport: (report) => { switch (report.type) { case CmcdRecordedRequestType.MANIFEST: manifestPanel.append(report); break; case CmcdRecordedRequestType.SEGMENT: segmentPanel.append(report); break; case CmcdRecordedRequestType.EVENT: eventPanel.append(report); break; } }, }); ``` The listener is bound to the attach lifecycle, so `detach()` clears it. To resume notifications after a detach and reattach, pass the callback again on the next `attach()` call. ## Stubbing event-mode POSTs CMCD v2 event reports are POSTed to a configured target URL. In tests, you usually do not want those requests to reach a real endpoint. The `eventTargetUrls` option intercepts matching POSTs and responds with a synthetic `204 No Content`. It still records the request body for inspection. ```typescript import { CmcdReportRecorder } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach({ eventTargetUrls: [ "https://events.example.com", "https://analytics.example.com", ], }); startPlayer(); const events = await recorder.waitForEvents(); console.log("Event report body:", events[0].request.body); recorder.detach(); ``` A request matches if its URL starts with any entry in the list. POSTs whose URLs do not match are not stubbed and pass through to the underlying transport. The recorder still records them if they have CMCD data. ## Combining recorded reports with validation `CmcdRecordedReport.request` is an `HttpRequest`. Pass it directly to `validateCmcdRequest` or the other validators in this package: ```typescript import { CmcdReportRecorder, validateCmcdRequest, } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach(); startPlayer(); const segments = await recorder.waitForSegments({ count: 3 }); for (const report of segments) { const result = validateCmcdRequest(report.request); if (!result.valid) { console.error(`Bad CMCD on ${report.request.url}:`, result.issues); } } recorder.detach(); ``` For event-mode reports, use `validateCmcdEventReport`: ```typescript import { CmcdReportRecorder, validateCmcdEventReport, } from "@svta/cml-cmcd"; const recorder = new CmcdReportRecorder(); recorder.attach({ eventTargetUrls: ["https://events.example.com"] }); startPlayer(); const events = await recorder.waitForEvents(); const result = validateCmcdEventReport(events[0].request); if (!result.valid) { console.error("Bad event report:", result.issues); } recorder.detach(); ``` ## Custom transports By default, the recorder patches `globalThis.XMLHttpRequest` and `globalThis.fetch`. If the player under test uses a different HTTP client, supply a custom `CmcdTransportAdapter` in the `transports` option. Examples are a custom wrapper, `node-fetch`, and `undici`. A transport adapter installs its patch, calls the supplied `deliver` function for every outbound request, and returns a teardown function that removes the patch: ```typescript import { CmcdReportRecorder, type CmcdTransportAdapter, type CmcdRequestDeliver, } from "@svta/cml-cmcd"; import type { HttpRequest } from "@svta/cml-utils"; function createMyClientTransport(): CmcdTransportAdapter { return { attach(deliver: CmcdRequestDeliver): () => void { const original = myClient.send; myClient.send = (req: MyRequest) => { const httpRequest: HttpRequest = normalize(req); const stub = deliver(httpRequest); if (stub) { // Event-target match: return the synthetic 204 response return stub; } return original.call(myClient, req); }; return () => { myClient.send = original; }; }, }; } const recorder = new CmcdReportRecorder(); recorder.attach({ transports: [createMyClientTransport()], }); ``` The `deliver` function returns a `Response` only when the request matched `eventTargetUrls`. In that case the adapter must return the synthetic response instead of forwarding the request to the underlying client. ## Lifecycle and teardown `attach()` does nothing when the recorder is already attached, so it is safe to call once per test setup. `detach()`: - Removes the transport patches and restores the original `XMLHttpRequest` and `fetch` references - Clears the `onReport` listener - Rejects any pending `waitFor*` promises with `Error('Recorder detached while waiting')` - Does **not** clear the recorded report buffer. Call `clear()` for that Always pair `attach()` with `detach()` in your test teardown (`afterEach` or equivalent). A recorder that remains attached keeps patching `XMLHttpRequest` and `fetch` for the rest of the process, which breaks later tests. ## Tips - **Fill a live view from existing reports.** When you open an inspection panel during a session, call `getReports()` once after `attach()` to fill the table. Then use `onReport` for the updates. - **Filter inside `onReport`.** The listener does not accept a type filter. If you need only a subset, branch on `report.type` or `report.reportingMode` inside the callback. - **Use `clear()` between test phases.** When one test runs several playback scenarios, call `recorder.clear()` between phases to reset the buffer without a reattach. - **Default timeout is 15 seconds.** For fast unit-style tests, set a shorter default for the recorder with the `waitTimeout` attach option. Or pass an explicit `timeout` to one `waitFor*` call. - **Body normalization.** The transports read POST bodies into strings at capture time, so `report.request.body` is always a string or `undefined`, never a `ReadableStream`. Pass it directly to `validateCmcdEventReport`. --- title: User Guide description: How to use the CMCD reporter --- # CMCD User Guide The `CmcdReporter` class manages CMCD reporting in a video player from one place. It handles request-mode reporting, which adds CMCD data to media requests, and event-mode reporting, which sends periodic reports to analytics endpoints. ## Installation ```bash npm install @svta/cml-cmcd ``` ## Basic Usage ### Instantiating the Reporter Create a new `CmcdReporter` instance with configuration options: ```typescript import { CmcdReporter, CmcdEventType, CMCD_QUERY, CMCD_V2, } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ // Optional: Provide a content ID for the current asset cid: "content-12345", // Optional: Provide a session ID (auto-generated if not provided) sid: "session-abc-123", // Optional: CMCD version (defaults to CMCD_V2) version: CMCD_V2, // Optional: Transmission mode for request reports (defaults to 'query') transmissionMode: CMCD_QUERY, // Optional: Keys to include in request reports (if not provided, request reporting is disabled) enabledKeys: ["br", "bl", "d", "ot", "sid", "cid", "mtp", "sf", "st"], // Event reporting targets eventTargets: [ { url: "https://analytics.example.com/cmcd", // The events to report. If not provided, the event target is disabled. events: [ CmcdEventType.PLAY_STATE, CmcdEventType.ERROR, CmcdEventType.TIME_INTERVAL, ], interval: 30, // seconds between TIME_INTERVAL reports batchSize: 5, // number of events to batch before sending enabledKeys: ["br", "bl", "sid", "cid"], }, ], }); ``` ### Minimal Configuration For basic request-mode reporting without event reporting: ```typescript import { CmcdReporter } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "my-video-id", }); ``` > [!NOTE] > The library defaults to the latest CMCD version (`CMCD_V2`). If you upgrade the library and need an older CMCD version, set the `version` option, such as `version: CMCD_V1`. ## Setting CMCD Data Use the `update()` method to set or change CMCD data. The reporter keeps the values in its data store and applies them to all later requests and events. ```typescript // Update multiple fields at once reporter.update({ br: [5000], // Encoded bitrate (kbps) d: 4000, // Segment duration (ms) ot: "v", // Object type: video bl: [25000], // Buffer length (ms) mtp: [12000], // Measured throughput (kbps) sf: "d", // Streaming format: DASH st: "v", // Stream type: VOD }); // Update individual fields as playback state changes reporter.update({ bl: [30000] }); // Buffer increased reporter.update({ pr: 1.5 }); // Playback rate changed reporter.update({ bs: true }); // Buffer starvation occurred ``` > [!NOTE] > In CMCD v2, several keys that were plain numbers in v1 have the type `CmcdObjectTypeList`. This type is an inner list that supports an annotation per object type. Pass these keys as arrays, such as `br: [5000]`: `br`, `bl`, `mtp`, `tb`, `lb`, `ab`, `tab`, `lab`, `tbl`, `pb`, `bsa`, `bsd`, `bsda`, and `tpb`. ### Value Formatting CTA-5004 and CTA-5004-B define every numeric key as an integer, except `pr`. `CmcdReporter` and the encoding functions, such as `encodeCmcd`, format each value before transmission. Pass raw values in their base units. Do not round or truncate values yourself. The table shows the formatting rules: | Keys | Formatting | | ------------------------------- | ------------------------------ | | `bl`, `dl`, `mtp`, `rtp`, `tbl` | Rounded to the nearest 100 | | `pr` | Not rounded | | Every other numeric key | Rounded to the nearest integer | For example, pass the exact buffer length in milliseconds and the exact bitrate in kbps: ```typescript reporter.update({ bl: [25432], br: [2500.7] }); // sent as bl=(25400) and br=(2501) ``` ### Keys the Reporter Omits The reporter applies the "MUST NOT" rules of CTA-5004-B when it encodes a report. It omits a key without an error in these cases: - The value is `undefined`, `null`, an empty string, an empty array, or a number that is not finite. The specification requires the key to be absent when the value is unknown. A `false` value is also omitted. The exception is a `bg: false` that a transform adds to a backgrounded-mode report (see [Recording Events](#recording-events)). - The value does not match the type of its key, such as `pt: "123"` or `sid: 123`. In a list, the reporter drops each element of the wrong type, such as `null` in `pb: [2500, null]`. It omits the key when no element is left. - The value is a string longer than the maximum of its key, such as a `sid` of more than 64 characters. - `d` when `ot` is not `a`, `v`, `av`, `tt`, `c`, or `o`. For example, a manifest request with `ot: "m"` never carries `d`. The rule uses the object type you set, even if `ot` is not in `enabledKeys`. - `tpb` when `ot` is not `a`, `v`, `av`, or `c`. - `ab`, `lab`, or `tab` when the same report also carries `br`, `lb`, or `tb`. The report keeps the exact bitrate. - `cen` on every event except a custom event. - `cmsdd`, `cmsds`, `rc`, `smrt`, `ttfb`, `ttfbb`, `ttlb`, and `url` on every report except a response-received report. - `pr` when the value is `1`, except on a playback rate event. When `ot` is absent, `d` and `tpb` are sent unchanged. The validators report the same conflicts as errors. See the [Validation Guide](https://cml.svta.org/llms/cmcd/docs/validation-guide.md#version-specific-behavior). These rules read the value inside an `SfItem`. For example, the reporter omits a `pr` of `1` with a parameter, as it omits `pr: 1`. > [!NOTE] > Version 1 output follows CTA-5004. `cid` has at most 64 characters, and a custom string has no length limit. Version 1 has no `st=ll` and no `sf=e`, so the reporter sends `st=l` and `sf=o`. ### Parameterized Values with toCmcdValue In CMCD v2, some keys support values with parameters, such as `nor` (next object request) and `br` (encoded bitrate). Use the `toCmcdValue` helper function to attach parameters to a value: ```typescript import { CmcdReporter, toCmcdValue } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", }); // Using toCmcdValue to add byte-range parameters to next object requests reporter.update({ nor: [ toCmcdValue("segment_002.m4s", { r: "0-50000" }), toCmcdValue("segment_003.m4s", { r: "0-50000" }), ], }); // This encodes to: nor=("segment_002.m4s";r="0-50000" "segment_003.m4s";r="0-50000") ``` The `toCmcdValue` utility accepts two arguments: 1. **value** - The bare item value, such as a string, number, or boolean 2. **params** - An object of key-value pairs for the parameters ```typescript import { toCmcdValue } from "@svta/cml-cmcd"; reporter.update({ nor: [ toCmcdValue("seg1.m4s", { r: "0-5000" }), toCmcdValue("seg1.m4s", { r: "5000-10000" }), ], }); // This encodes to: nor=("seg1.m4s";r="0-5000" "seg1.m4s";r="5000-10000") ``` Many keys accept a list of numeric values, each with an object type given as a boolean parameter. ```typescript import { toCmcdValue } from "@svta/cml-cmcd"; reporter.update({ br: [toCmcdValue(5000, { v: true }), toCmcdValue(3000, { a: true })], }); // This encodes to: br=(5000;v 3000;a) ``` > [!NOTE] > CMCD version 1 has one number for each of these keys. For version 1, the reporter sends the value that has the object type (`ot`) of the request. It uses a value without an object type for every `ot`. If no value matches, it omits the key. For example, the list above gives `br=3000` for an audio request and no `br` for a manifest request. `decodeCmcd` keeps the parameters of a member. A member with parameters becomes an `SfItem`. For an inner list with parameters, such as `br=(3000 6000);p=2`, the value of the `SfItem` is the whole list. The encoder writes the parameters again, and the validators check the values inside the list: ```typescript import { decodeCmcd, encodeCmcd, validateCmcd } from "@svta/cml-cmcd"; const data = decodeCmcd("br=(3000 6000);p=2,v=2", { convertToLatest: true }); console.log(encodeCmcd(data)); // br=(3000 6000);p=2,v=2 console.log(validateCmcd(data).valid); // true ``` ### Absolute URLs for `nor` The CMCD specification defines `nor` (next object request) as a path relative to the current request URL. `CmcdReporter` also accepts absolute URLs and converts them. It emits same-origin URLs as relative paths. It passes cross-origin values, and values that are already relative, through unchanged. ```typescript // When the current request URL is: // https://cdn.example.com/streams/video/segment_001.m4s // either form below produces the same encoded output. // Already relative (per spec): reporter.update({ nor: ["segment_002.m4s"], }); // Absolute URL, converted automatically: reporter.update({ nor: ["https://cdn.example.com/streams/video/segment_002.m4s"], }); // Both encode to: nor=("segment_002.m4s") ``` ## Custom Keys CMCD allows player-defined key/value pairs next to the standard keys. Per CTA-5004-B, a custom key name MUST have a hyphenated prefix, so that it cannot collide with keys in future revisions of the specification. Clients SHOULD use reverse domain name syntax (reverse-DNS) for their prefix, such as `com.example-mykey`. ### Naming Rules The `CmcdCustomKey` type requires a lowercase hyphenated string at compile time. At runtime, `isCmcdCustomKey` enforces the full rule. A key needs a lowercase first letter, then characters from `a-z 0-9 . -`. A hyphen must be neither the first nor the last character. Keys that fail the rule are dropped without an error. The rule is the CTA-5004-B custom key rule, restricted to names that RFC 8941 key serialization accepts. So a key that passes the check always reaches the wire. In practice: **use lowercase reverse-DNS names**. ### Enabling Custom Keys Custom keys pass through the same `enabledKeys` filter as standard keys: the top-level `enabledKeys` for request reports, and each target's `enabledKeys` for event reports. There is no wildcard. A custom key that is not listed is dropped without an error. ```typescript import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", // Request mode: the custom key must be listed here enabledKeys: ["br", "bl", "sid", "cid", "com.example-experiment"], eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.TIME_INTERVAL, CmcdEventType.ERROR], // Event mode: and here, for each target that should receive it enabledKeys: ["br", "bl", "sid", "cid", "com.example-experiment"], }, ], }); // Persist the value; it rides all subsequent request and event reports reporter.update({ "com.example-experiment": "variant-b" }); // Or attach it to a single event report only reporter.recordEvent(CmcdEventType.ERROR, { ec: ["FATAL"], "com.example-experiment": "variant-b", }); ``` ### Value Types and Wire Format Custom values may be strings, numbers, booleans, or tokens. Wrap a value with `toCmcdValue()` to attach structured field parameters (see the `CmcdCustomValue` type). Notes on the wire format: - `true` is encoded as a bare key without a value (`com.example-flag`), per the RFC 8941 boolean convention. `false` is treated like other empty values and dropped. - In version 2, a string value has at most 64 characters. The reporter drops a longer string. It also drops a value of another type, such as a plain object. - Values that RFC 8941 cannot serialize currently make encoding throw. Examples are strings with control characters and integers outside ±999,999,999,999,999. Validate custom values before you pass them to the reporter. Graceful handling of such values is tracked in [#327](https://github.com/streaming-video-technology-alliance/common-media-library/issues/327). - The validators in this package expect custom values to be strings of at most 64 characters. Prefer short string values for interoperability. See the [Validation Guide](https://cml.svta.org/llms/cmcd/docs/validation-guide.md#custom-keys). ### Custom Keys in Headers Mode In headers transmission mode, custom keys go in the `CMCD-Request` header by default. Use the `customHeaderMap` option to route custom keys to other CMCD header shards. For example, a session-scoped custom key belongs in `CMCD-Session`, so intermediaries can treat it as constant for the session: ```typescript import { CmcdReporter, CMCD_HEADERS } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", transmissionMode: CMCD_HEADERS, enabledKeys: ["sid", "cid", "com.example-experiment"], customHeaderMap: { "CMCD-Session": ["com.example-experiment"], }, }); reporter.update({ "com.example-experiment": "variant-b" }); const request = reporter.createRequestReport({ url: "https://cdn.example.com/video/segment_001.m4s", }); // request.headers will contain: // { // 'CMCD-Session': 'cid="video-123",com.example-experiment="variant-b",sid="…",v=2', // } ``` Standard keys have fixed shards per the CMCD specification and cannot be moved. The option has no effect in query transmission mode or on event reports, which POST a body and have no header shards. ## Recording Events `update()` fires the state-change events (`PLAY_STATE`, `PLAYBACK_RATE`, `CONTENT_ID`, `BACKGROUNDED_MODE`, `BITRATE_CHANGE`) whenever a tracked key's value differs from the last reported value. Use `update()` as the one entry point for these events. ```typescript import { CmcdEventType } from "@svta/cml-cmcd"; reporter.update({ sta: "p" }); // → fires PLAY_STATE reporter.update({ pr: 1.5 }); // → fires PLAYBACK_RATE reporter.update({ cid: "movie-42" }); // → fires CONTENT_ID reporter.update({ bg: true }); // → fires BACKGROUNDED_MODE reporter.update({ bg: false }); // → fires BACKGROUNDED_MODE without bg reporter.update({ br: [5000] }); // → fires BITRATE_CHANGE // Consecutive updates with the same value are deduplicated. reporter.update({ sta: "p" }); // dropped (unchanged) ``` A `BACKGROUNDED_MODE` report with `bg` is the entry to backgrounded mode. A report without `bg` is the exit, as CTA-5004-B defines it. If the session has not reported `bg: true`, `update({ bg: false })` fires no event. The exception is a session that starts while `bg` is `true`, after a `sid` change. If a collector expects `bg=?0` on the exit, add `bg: false` to the exit report in the target's transform. The encoder writes that value as `?0`: ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ eventTargets: [ { url: "https://legacy-collector.example.com/cmcd", events: [CmcdEventType.BACKGROUNDED_MODE], enabledKeys: ["bg", "sid", "v", "e", "ts", "sn"], transform: (data) => data.e === CmcdEventType.BACKGROUNDED_MODE && !("bg" in data) ? { ...data, bg: false } : data, }, ], }); ``` ### Snapshot context on state changes State-change events report whatever is in the data store at that moment. To attach snapshot context, either combine the continuous metrics with the state key in one `update()` call: ```typescript reporter.update({ sta: "p", bl: [3000], mtp: [8500], pt: 12500 }); // → fires PLAY_STATE with sta="p", bl=[3000], mtp=[8500], pt=12500 ``` Or set the metrics with earlier `update()` calls, so they are already in the data store when the state key changes: ```typescript // Keep metrics fresh as the player computes them reporter.update({ bl: [3000], mtp: [8500], pt: 12500 }); // Later, when state changes reporter.update({ sta: "p" }); // → fires PLAY_STATE with sta="p", bl=[3000], mtp=[8500], pt=12500 ``` The same pattern keeps `TIME_INTERVAL` reports useful. The reporter emits those events on a timer with whatever is in the data store at that moment. There is no callback for per-event data. Keep the continuous metrics current with `update()`. ### Do not pair `update()` with `recordEvent()` for the same state-change field Dedup drops the payload of the second call: ```typescript import { CmcdEventType } from "@svta/cml-cmcd"; reporter.update({ sta: "p" }); // auto-fires PLAY_STATE reporter.recordEvent(CmcdEventType.PLAY_STATE, { // suppressed: sta: "p", // sta unchanged bl: [3000], // bl never emitted }); ``` Use the single-call form shown above instead. ### Using `recordEvent()` `recordEvent()` is for events whose payload belongs to the event call itself. These events do not represent a stored state transition: ```typescript import { CmcdEventType } from "@svta/cml-cmcd"; // Custom event: the name and any payload only make sense for this call. // See "Custom Events" below for the required target configuration. reporter.recordEvent(CmcdEventType.CUSTOM_EVENT, { cen: "ad-quartile" }); // Error: ec carries the player error code(s). Per CTA-5004-B the list // notation is required even for a single code. reporter.recordEvent(CmcdEventType.ERROR, { ec: ["FATAL"] }); // Ad lifecycle, mute/unmute, player expand/collapse, skip. reporter.recordEvent(CmcdEventType.AD_START, { /* ad metadata */ }); reporter.recordEvent(CmcdEventType.MUTE); ``` For `RESPONSE_RECEIVED`, prefer `recordResponseReceived()` (see below). It derives the per-response keys for you. > [!NOTE] > The response-received keys (`url`, `rc`, `ttfb`, `ttlb`, `ttfbb`, `cmsdd`, `cmsds`, `smrt`) are valid only on `RESPONSE_RECEIVED` reports. If you pass them in the `data` of any other event, the reporter removes them, even when `enabledKeys` lists them. Custom hyphenated keys are not affected. ### Custom Events Custom events (`e=ce`) report player-defined events by name in the `cen` key, a string of at most 64 characters. Two configuration rules apply: - An event target receives custom events only if its `events` array lists `CmcdEventType.CUSTOM_EVENT`. Otherwise the target drops the event without an error. - `cen` is always included on custom events and does not need to be in `enabledKeys`. Any other payload, including custom keys, still passes through the target's `enabledKeys`. ```typescript import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", eventTargets: [ { url: "https://analytics.example.com/cmcd", // CUSTOM_EVENT must be listed for the target to receive it events: [CmcdEventType.CUSTOM_EVENT], // cen is force-included; the custom payload key must be enabled enabledKeys: ["sid", "cid", "com.example-quartile"], }, ], }); reporter.recordEvent(CmcdEventType.CUSTOM_EVENT, { cen: "ad-quartile", "com.example-quartile": "q3", }); // The event report contains: // cen="ad-quartile",cid="video-123",com.example-quartile="q3",e=ce,sid=…,ts=…,v=2 ``` Per CTA-5004-B, when a custom event transfers a value, the custom key name SHOULD relate to the custom event name. Above, `com.example-quartile` relates to `ad-quartile`. See [Custom Keys](#custom-keys) for the naming rules. ### Recording Response Received Events The `recordResponseReceived()` method records `RESPONSE_RECEIVED` events and derives the timing metrics from the HTTP response. It is usually called after a segment request completes. ```typescript import { CmcdReporter, CmcdEventType } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", enabledKeys: ["br", "d", "ot", "url", "rc", "ttfb", "ttlb"], eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.RESPONSE_RECEIVED], enabledKeys: ["url", "rc", "ttfb", "ttfbb", "ttlb", "cmsdd", "cmsds", "smrt", "br", "d", "ot"], }, ], }); ``` #### Basic Usage The method accepts a `CommonMediaResponse` object and derives the following keys: | Key | Description | Source | | ------ | -------------------------------- | -------------------------------------------------- | | `url` | The requested URL (without CMCD) | `response.request.url` (CMCD query param stripped) | | `rc` | HTTP response status code | `response.status` | | `ts` | Request initiation timestamp | `response.resourceTiming.startTime` | | `ttfb` | Time to first byte (ms) | `responseStart - startTime` | | `ttlb` | Time to last byte (ms) | `response.resourceTiming.duration` | ```typescript // After receiving a response const response = { status: 200, request: decoratedRequest, resourceTiming: { startTime: performance.now(), responseStart: performance.now() + 50, duration: 150, encodedBodySize: 512000, }, }; reporter.recordResponseReceived(response); ``` #### Complete Request/Response Flow For full request and response tracking, call `createRequestReport()` before the request and `recordResponseReceived()` after the response. The response event includes the CMCD data from the original request: ```typescript async function fetchSegment( url: string, segmentInfo: SegmentInfo, ): Promise { // Update player state reporter.update({ bl: [player.getBufferLength()], mtp: [player.getMeasuredThroughput()], }); // Decorate the request with CMCD data const request = reporter.createRequestReport( { url, method: "GET" }, { br: [segmentInfo.bitrate], d: segmentInfo.duration, ot: segmentInfo.type, }, ); // Fetch the segment and capture timing const startTime = performance.now(); const response = await fetch(request.url, request); const responseStart = performance.now(); const buffer = await response.arrayBuffer(); const duration = performance.now() - startTime; // Record the response received event reporter.recordResponseReceived({ status: response.status, request, resourceTiming: { startTime, responseStart, duration, encodedBodySize: buffer.byteLength, }, }); return buffer; } ``` #### Providing Additional Data You can supply CMCD keys that the method cannot derive. The keys `cmsdd`, `cmsds`, and `smrt` are strings. Each one holds a Base64 copy of the data in a header: ```typescript reporter.recordResponseReceived(response, { ttfbb: 25, // Time to first body byte (player-measured) cmsdd: "ImNkbi1hIjtldHA9OTY7cnR0PTg=", // Base64 copy of the CMSD-Dynamic response header cmsds: "b3Q9dixzZj1oLHN0PXYsZD00MDAwLGJyPTI1MDA=", // Base64 copy of the CMSD-Static response header smrt: "KCk7bj1PcmlnaW5BO3N5bg==", // Base64 copy of the response tracing data, from the Request Tracing header }); ``` Values in the `data` parameter override derived values. ## Decorating Segment Requests Use the `createRequestReport()` method to add CMCD data to segment requests. The method returns a new request object with the CMCD data in query parameters or in headers, depending on the configuration. ### Query Parameter Mode (Default) ```typescript import { CmcdReporter, CMCD_QUERY } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ transmissionMode: CMCD_QUERY, }); // Update CMCD data before the request reporter.update({ br: [5000], bl: [25000], d: 4000, ot: "v", }); // Create the original request const request = { url: "https://cdn.example.com/video/segment_001.m4s", method: "GET", headers: {}, }; // Create the decorated request const decoratedRequest = reporter.createRequestReport(request); // Use the decorated request with your HTTP client fetch(decoratedRequest.url, decoratedRequest); ``` `createRequestReport()` adds the `CMCD` parameter and makes no other change to the URL. If the URL already has a `CMCD` parameter, the new value replaces it. The value uses the encoding of the CTA-5004-B query examples. Only letters, digits, `-`, `.`, `_`, and `~` stay unencoded. ### Headers Mode ```typescript import { CmcdReporter, CMCD_HEADERS } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ transmissionMode: CMCD_HEADERS, }); reporter.update({ br: [5000], bl: [25000], d: 4000, }); const request = { url: "https://cdn.example.com/video/segment_001.m4s", method: "GET", headers: {}, }; const decoratedRequest = reporter.createRequestReport(request, { ot: "v" }); // decoratedRequest.headers will contain: // { // 'CMCD-Object': 'br=(5000),d=4000,ot=v', // 'CMCD-Request': 'bl=(25000)', // } ``` ## Lifecycle Management ### Starting the Reporter Call `start()` to begin automatic `TIME_INTERVAL` event reporting: ```typescript reporter.start(); ``` The timer fires at the configured interval. ### Stopping the Reporter Call `stop()` to stop automatic reporting: ```typescript reporter.stop(); ``` ### Flushing Events Call `flush()` to send all queued events now, regardless of the batch size: ```typescript // Send all pending events (useful when playback ends) reporter.flush(); ``` ### Session Changes and Late Responses Changing the session ID starts a new session. Sequence numbers restart, the media start delay (`msd`) gate re-arms, state-change dedup baselines clear, and targets disposed by an HTTP 410 are active again. The data store is kept, so `cid`, `br`, custom keys, and the rest survive the change. ```typescript reporter.update({ sid: "next-session" }); ``` The reporter retains state for recently ended sessions. `sessionRetention` sets how many, and `0` disables retention. So a media request that completes after a session change reports under the session that issued it. The report uses that session's `sid`, sequence numbers, and data snapshot. The snapshot is frozen at the transition. Mutating an array that you passed to `update()` earlier does not change the late reports of an ended session. Attribution uses a frozen provenance record (`CmcdRequestProvenance`). `createRequestReport()` stores the record on the request's `customData` under the exported `CMCD_REQUEST_PROVENANCE` symbol, on every request it returns, including requests it does not decorate. The record contains: - the `sid` of the issuing session, which is the attribution key - the `cid` that was current when the request was issued - the per-call data of the request, as an encoded CMCD string The reporter rebuilds a `RESPONSE_RECEIVED` event from the record, with the record's `cid` and the caller's request-time inputs. So a response that completes after a session or content change keeps the meaning it had when its request was sent. The `sid` itself is the session identity. CTA-5004-B expects it to be unique per playback session, so generate a new UUID for every session. > [!WARNING] > Never reuse a session ID. Everything session-scoped depends on the `sid`, so reusing one corrupts the collected data. The reporter replaces the retained session that has the same `sid`. The late responses of the replaced session are then relabeled onto the replacement, and they consume the replacement's sequence numbers and gates. Spread and `Object.assign` keep the record in ordinary request clones. `JSON.stringify` and structured clone drop symbol-keyed properties. A request that crosses such a boundary, such as a worker or a JSON cache, needs the record stored next to it and restored afterwards: ```typescript import { CMCD_REQUEST_PROVENANCE } from "@svta/cml-cmcd"; // Symbol keys do not survive JSON, so carry the record explicitly. The // record itself is JSON-safe. const payload = JSON.parse( JSON.stringify({ request, provenance: request.customData[CMCD_REQUEST_PROVENANCE], }), ); // Restore it before reporting the response and attribution stays exact. payload.request.customData[CMCD_REQUEST_PROVENANCE] = payload.provenance; reporter.recordResponseReceived({ status: 200, request: payload.request }); ``` The record is the only key that selects an ended session. A response whose request has no record reports under the current session, as in version 2.4.0. The request-time data then comes from `customData.cmcd`. A per-call `data.sid` is no substitute. The reporter accepts any record whose `sid` resolves, so you can also construct one. A hand-built request, or a request decorated by another reporter configured with the same session, attributes by naming the `sid`: ```typescript import { CMCD_REQUEST_PROVENANCE } from "@svta/cml-cmcd"; // The request was never decorated, so the player names the session. request.customData[CMCD_REQUEST_PROVENANCE] = { sid: "previous-session" }; reporter.recordResponseReceived({ status: 200, request }); ``` A response attributed to a session that is no longer retained is dropped, not relabeled with the current session ID. A session change sends the unsent event reports of an ended session before eviction can discard them. Each report keeps the `sid` and sequence number of its own session. A failed batch that is re-queued into an evicted session is discarded with the session. ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.TIME_INTERVAL, CmcdEventType.PLAY_STATE], interval: 30, batchSize: 3, }, ], }); // Start reporting when playback begins function onPlaybackStart() { reporter.start(); } // Stop reporting and flush when playback ends function onPlaybackEnd() { reporter.stop(); reporter.flush(); } // Clean up when the player is destroyed function onPlayerDestroy() { reporter.stop(); reporter.flush(); } ``` ## Custom HTTP Client By default, `CmcdReporter` uses the native `fetch` API to send event reports. You can provide a custom requester function: ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; import type { HttpRequest } from "@svta/cml-utils"; const customRequester = async (request: HttpRequest) => { // Use your preferred HTTP client const response = await axios({ url: request.url, method: request.method, headers: request.headers, data: request.body, }); return { status: response.status }; }; const reporter = new CmcdReporter( { cid: "video-123", eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.TIME_INTERVAL], }, ], }, customRequester, ); ``` ## Filtering CMCD Keys The `enabledKeys` option limits which CMCD keys the reports include: ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", // Only include these keys in request reports enabledKeys: ["br", "bl", "d", "ot", "sid", "cid"], eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.TIME_INTERVAL], // Override enabled keys for this specific target enabledKeys: ["br", "bl", "sid", "cid"], }, ], }); ``` > [!NOTE] > The CMCD specification requires certain keys, such as `v` (version). Reports always include them, whatever the `enabledKeys` setting. ## Transforming and Cancelling Reports `enabledKeys` decides which keys a destination may ever receive. When a decision depends on the individual report, such as this request, this response, or this target, use a `transform` instead. A transform is a synchronous function. It receives the assembled CMCD data and the associated media request, and it returns the data to send the report or `null` to cancel it: ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; const reporter = new CmcdReporter({ cid: "video-123", enabledKeys: ["br", "bl", "sid", "cid", "v", "sn"], // Request mode: never decorate license requests with CMCD transform: (data, request) => request.url.includes("/license") ? null : data, eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.RESPONSE_RECEIVED], enabledKeys: ["url", "rc", "sid", "cid", "v", "e", "ts", "sn"], // Event mode, this target only: report segment responses only transform: (data, request) => request?.customData?.["requestType"] === "segment" ? data : null, }, ], }); ``` The bracket access on the last line is the default, because the library cannot know the player's data type. [Typing `customData`](#typing-customdata) removes it. Placement determines scope, the same way it does for `enabledKeys`: | Placement | Applies to | | ------------------------------ | -------------------------------------------- | | `transform` at the top level | Request reports from `createRequestReport()` | | `transform` on an event target | Event reports bound for that target | By design, there is no single function for both paths. An event report has its own type in `data.e`. The target's configuration is available where you write its transform, and the transform can close over anything else it needs. A policy that applies in more than one place is a shared function that you reference from each placement: ```typescript import type { Cmcd } from "@svta/cml-cmcd"; import { CmcdEventType } from "@svta/cml-cmcd"; const sampled = Math.random() < 0.1; const sampleIntervals = (data: Cmcd): Cmcd | null => data.e === CmcdEventType.TIME_INTERVAL && !sampled ? null : data; ``` To combine several concerns at one placement, compose the functions yourself: ```typescript import type { Cmcd } from "@svta/cml-cmcd"; import type { HttpRequest } from "@svta/cml-utils"; const scrubPii = (data: Cmcd, request: HttpRequest | undefined): Cmcd | null => request?.url.includes("/private/") ? null : { ...data, cid: undefined }; const transform = (data: Cmcd, request: HttpRequest | undefined): Cmcd | null => { const scrubbed = scrubPii(data, request); return scrubbed === null ? null : sampleIntervals(scrubbed); }; ``` ### The associated request The second argument is the media request that the report belongs to. In request mode the request is always present, and it is the object you passed to `createRequestReport()`. Mutating that object does not change the returned report. In event mode the request is present for events recorded through `recordResponseReceived()`. It is `undefined` for everything else, including state-change events from `update()` and periodic `TIME_INTERVAL` reports. Player-specific request categories belong on the request. CMCD has no concept of a "segment request" or an "init request". A player that wants to filter on such a category puts it on `request.customData` and reads it back in the transform. For the narrower question of manifest or media, the `ot` key already answers in CMCD terms when the player sets it: `data.ot === "m"` is a manifest. The request is a read-only view (`CmcdTransformRequest`). It is context for the decision, not something to change, and every member is `readonly`. The types cannot cover two cases. A mutable body such as `FormData` or `URLSearchParams` has mutating methods of its own, and JavaScript callers get no compile-time enforcement. Mutating the request in either way is unsupported, and the outgoing report may reflect the mutation. Treat the request as immutable, whatever the compiler can prove. ### Typing `customData` By default, `customData` values are `unknown`, which forces the bracket access above. Describe the player's data type once, and those reads become ordinary dot access, checked at compile time. Annotating one transform is enough. The rest of the configuration infers the same type, including the top-level `transform`. ```typescript import { CmcdEventType, CmcdReporter } from "@svta/cml-cmcd"; import type { CmcdEventReportTransform } from "@svta/cml-cmcd"; // The player's own request taxonomy, carried on customData. type PlayerData = { requestType: "segment" | "manifest" | "license" }; // The only annotation in the configuration. const segmentsOnly: CmcdEventReportTransform = (data, request) => request?.customData?.requestType === "segment" ? data : null; const reporter = new CmcdReporter({ cid: "video-123", enabledKeys: ["br", "bl", "sid", "cid", "v", "sn"], // Not annotated, but `customData` is typed here too, so a misspelt // "licence" is a compile error rather than a filter that never matches. transform: (data, request) => request.customData?.requestType === "license" ? null : data, eventTargets: [ { url: "https://analytics.example.com/cmcd", events: [CmcdEventType.RESPONSE_RECEIVED], enabledKeys: ["url", "rc", "sid", "cid", "v", "e", "ts", "sn"], transform: segmentsOnly, }, ], }); ``` `new CmcdReporter({ ... })` states the same type explicitly. That form is clearer when no transform is annotated. Either way, the cost is one annotation per reporter, not one per transform. Without any annotation, nothing changes from the previous section: values remain `unknown` and bracket access still works. Once a reporter has a type, `createRequestReport()` and `recordResponseReceived()` require the requests you pass to satisfy it. A typo, or a request from a different code path, is a compile error at the call site. Without the type, the transform would read `undefined` and cancel the report without an error: ```typescript import { CmcdReporter } from "@svta/cml-cmcd"; type PlayerData = { requestType: "segment" | "manifest" | "license" }; const reporter = new CmcdReporter({ cid: "video-123" }); // Error: 'requestTypo' does not exist in type 'PlayerData' reporter.createRequestReport({ url: "https://cdn.example.com/segment.mp4", // @ts-expect-error customData: { requestTypo: "segment" }, }); ``` `customData` is readonly at every depth, not only at the top level. Describing a nested type does not cost you the [read-only guarantee](#the-associated-request): ```typescript import type { CmcdEventReportTransform } from "@svta/cml-cmcd"; type PlayerData = { timing: { start: number } }; const transform: CmcdEventReportTransform = (data, request) => { const customData = request?.customData; // Error: cannot assign to 'start' because it is a read-only property // @ts-expect-error if (customData) customData.timing.start = 0; return data; }; ``` ### The data you receive is yours The first argument is a copy made for this one report. You can mutate it in place without affecting the data store or the reports for other targets. The same is true for nested values. Array keys such as `br` and `ec` are copied, so `data.ec.push("E100")` is safe, and so is changing the `params` of an `SfItem` inside one. ### What a transform cannot change After your transform returns, the reporter sets `e` and `sid` again and assigns the sequence number `sn` and `msd`. So a transform cannot change the event type of a report to pass a target's `events` filter. It cannot substitute the session ID, create gaps in the sequence numbers, or replay the media start delay marker. Cancelling a report consumes neither a sequence number nor `msd`. Wire `sn` values remain contiguous per destination, and `msd` goes on the next report that is sent. A transform also cannot remove a key that the event requires. Every event needs `e` and `ts`. State-change events need the key they signal (`sta`, `pr`, `cid`, `bg`, `br`). Custom events need `cen`, error events need `ec`, and response-received events need `url`. If your transform drops one of these keys, or replaces its value with `false` or an empty value, the reporter restores the value it had before. The reporter does not invent values. A required key that was already missing before your transform ran remains missing. The exit from backgrounded mode has no `bg` by design. For the other events, a missing required key is a bug at the call site, not something the transform did. Configuration cannot remove required keys either. The reporter includes them after the `enabledKeys` filter, so omitting one from `enabledKeys` does not suppress it. The payload remains valid. If a destination must not receive a key that an event requires, leave that event out of the target's `events`. Do not try to remove the key. `enabledKeys` is still the allowlist for the wire, and it still runs after the transform. A key that your transform adds must also be enabled at the same placement to reach the target. A key that the transform removes remains removed, unless the event requires it. Cancelling a state-change report does not undo dedup. The transition still happened, and the transform only suppressed its transmission. So the next `update()` with the same value is still deduplicated. Transforms change CMCD data only. They cannot modify the outgoing HTTP request. > [!IMPORTANT] > Transforms must not throw. An exception propagates to whatever called the reporter. For `TIME_INTERVAL` events the caller is the interval timer, so the exception becomes an unhandled error. The library does not catch these exceptions. Sending the data anyway would leak exactly the data that a redaction transform exists to remove. Dropping the data would make the loss impossible to debug. Wrap risky logic in `try` and `catch` and choose: return the data to send the report, or `null` to cancel it. A throw affects only the target whose transform threw. The other targets still receive the report, and queued batches are still sent. The error reaches your code after the reporter has finished with the event. Only the throwing target loses its report, and the loss consumes no sequence number. Without this isolation, one throwing transform would block every target configured after it. The reason is that the state-change dedup baseline commits before the reporter distributes the report to the targets, and the baseline is never undone. ## Configuration Reference ### CmcdReporterConfig | Property | Type | Default | Description | | ------------------ | ------------------------- | ------------------- | ------------------------------------------------------------------------------ | | `sid` | `string` | Auto-generated UUID | Session ID | | `cid` | `string` | `undefined` | Content ID | | `version` | `CmcdVersion` | `CMCD_V2` | CMCD protocol version | | `transmissionMode` | `CmcdTransmissionMode` | `'query'` | How to transmit CMCD data in request mode | | `customHeaderMap` | `Partial` | `undefined` | Routes [custom keys](#custom-keys-in-headers-mode) into specific CMCD header shards in headers mode. | | `enabledKeys` | `CmcdKey[]` | `undefined` | Keys to include in request reports. If not provided, no keys will be reported. [Custom keys](#custom-keys) must be listed explicitly. | | `eventTargets` | `CmcdEventReportConfig[]` | `[]` | Event reporting targets | | `sessionRetention` | `number` | `2` | Ended sessions to retain state for, in addition to the current one. `0` disables retention. See [Session Changes and Late Responses](#session-changes-and-late-responses). | | `transform` | `CmcdRequestReportTransform` | `undefined` | Transforms or cancels each request report. See [Transforming and Cancelling Reports](#transforming-and-cancelling-reports) and [Typing `customData`](#typing-customdata). | ### CmcdEventReportConfig | Property | Type | Default | Description | | ------------- | ----------------- | ----------- | --------------------------------------------------------------------------- | | `url` | `string` | Required | Analytics endpoint URL | | `events` | `CmcdEventType[]` | `undefined` | Events to report. If no events are provided, the target is disabled. | | `interval` | `number` | `30` | Seconds between TIME_INTERVAL reports | | `batchSize` | `number` | `1` | Events to batch before sending | | `version` | `CmcdVersion` | `CMCD_V2` | CMCD version for this target (must be v2 or higher) | | `enabledKeys` | `CmcdKey[]` | `undefined` | Keys to include for this target. If not provided, no keys will be reported. [Custom keys](#custom-keys) must be listed explicitly. | | `transform` | `CmcdEventReportTransform` | `undefined` | Transforms or cancels each of this target's event reports. See [Typing `customData`](#typing-customdata). | --- title: Validation Guide description: How to validate CMCD payloads --- # CMCD Validation Guide The `@svta/cml-cmcd` library provides composable validation functions. They verify that CMCD payloads conform to the CTA-5004 (v1) and CTA-5004-B (v2) specifications. Use them on the receiving side. Examples are an analytics server that collects CMCD event reports, and a CDN log processor that inspects CMCD request data. ## Overview There are four main validation functions, each for a different aspect of CMCD compliance: | Function | Purpose | | ------------------------- | ----------------------------------------------------------------- | | `validateCmcd` | Orchestrator: runs key, value, and structure checks | | `validateCmcdRequest` | Validates a `Request` or `HttpRequest` as request-mode data | | `validateCmcdEvents` | Validates a multi-line `application/cmcd` body as event-mode data | | `validateCmcdEventReport` | Validates a full `HttpRequest` as an event-mode payload | All validators return a `CmcdValidationResult`: ```typescript import type { CmcdValidationSeverity } from "@svta/cml-cmcd"; type CmcdValidationResult = { valid: boolean; // true if zero errors (warnings are OK) issues: CmcdValidationIssue[]; }; type CmcdValidationIssue = { key?: string; message: string; severity: CmcdValidationSeverity; // 'error' | 'warning' }; ``` ## Validating Event Report Payloads A common use case is validating CMCD v2 event reports received by POST. The payload is usually an `application/cmcd` body with newline-separated CMCD-encoded strings, or `application/json`. ### Using `validateCmcdEvents` The `validateCmcdEvents` function accepts a raw CMCD string and validates it as an event-mode payload. It supports multi-line `application/cmcd` bodies directly. It validates each non-empty line on its own and merges the results. An empty payload, with no non-empty lines, is an error. A body that ends with a line feed is an error. CTA-5004-B separates records with a single line feed and does not allow a trailing one. ```typescript import { validateCmcdEvents } from "@svta/cml-cmcd"; // Validate a complete application/cmcd POST body const body = `e=ps,sid="session-1",ts=1700000000000,sta=p,v=2 e=t,sid="session-1",ts=1700000001000,bl=(5000),v=2`; const result = validateCmcdEvents(body); if (!result.valid) { console.error("Invalid CMCD event:", result.issues); } ``` A single-line string works as well: ```typescript import { validateCmcdEvents } from "@svta/cml-cmcd"; const result = validateCmcdEvents( 'e=ps,sid="session-1",ts=1700000000000,sta=p,v=2', ); ``` ### Parsing and Validating `application/cmcd` manually The `validateCmcdEvents` function validates multi-line `application/cmcd` bodies directly. If you need to validate a single-line string, you can decode the CMCD string and validate the data yourself. `validateCmcdRequest` does the same internally. ```typescript import { decodeCmcd, validateCmcd } from "@svta/cml-cmcd"; // Simulated POST body (application/cmcd) const body = `e=ps,sid="session-1",ts=1700000000000,sta=p,v=2 e=t,sid="session-1",ts=1700000001000,bl=(5000),v=2`; // Parse each line and validate const lines = body.split("\n"); for (const line of lines) { const data = decodeCmcd(line); const result = validateCmcd(data, { reportingMode: "event" }); if (!result.valid) { console.error("Invalid CMCD event:", result.issues); } } ``` ## Validating Request Mode Data In request mode, CMCD data is attached to segment requests as query parameters or HTTP headers. The validator checks that event-only and response-only keys are absent. ### Using `validateCmcdRequest` The `validateCmcdRequest` function accepts a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object or an `HttpRequest` object from `@svta/cml-utils`. It checks for CMCD headers first. If it finds any CMCD header shards, it delegates to `validateCmcdHeaders`, which verifies the shard placement. Otherwise, it extracts the `CMCD` query parameter from the URL. A request that carries CMCD data in both the headers and the query parameter is an error. CTA-5004-B allows one transmission mode per request. The validator still checks the headers and returns their data. A `CMCD` parameter inside the URL fragment is ignored, because a server never receives the fragment. When it is the only CMCD data on the request, the error message names the fragment. ```typescript import { validateCmcdRequest } from "@svta/cml-cmcd"; // From a Request with CMCD headers const request = new Request("https://cdn.example.com/seg.mp4", { headers: { "CMCD-Object": "br=5000,d=4000,ot=v", "CMCD-Request": "bl=25000", "CMCD-Session": 'sid="abc"', }, }); const headerResult = validateCmcdRequest(request); // From a Request with CMCD in the query parameter const queryRequest = new Request( "https://cdn.example.com/seg.mp4?CMCD=br%3D5000%2Cbl%3D25000", ); const queryResult = validateCmcdRequest(queryRequest); // From an HttpRequest object const httpRequestResult = validateCmcdRequest({ url: "https://cdn.example.com/seg.mp4?CMCD=br%3D5000%2Cbl%3D25000", }); ``` ### From Query Parameters manually The `validateCmcdRequest` function validates the `CMCD` query parameter from the URL. If you need to validate the parameter yourself, extract it from the URL and validate the data with the `validateCmcd` function. ```typescript import { fromCmcdQuery, validateCmcd } from "@svta/cml-cmcd"; // Extract CMCD from a request URL's query string const url = new URL( "https://cdn.example.com/seg.mp4?CMCD=br%3D5000%2Cbl%3D25000%2Cd%3D4000%2Cot%3Dv%2Csid%3D%22abc%22", ); const data = fromCmcdQuery(url.searchParams); const result = validateCmcd(data, { reportingMode: "request" }); if (!result.valid) { console.error("Invalid CMCD request data:", result.issues); } ``` ### From HTTP Headers manually When CMCD is transmitted in HTTP headers, you can validate the headers yourself with the `validateCmcdHeaders` function. ```typescript import { validateCmcdHeaders } from "@svta/cml-cmcd"; // Pass raw header strings directly — decoding is handled internally. // Request mode validation is applied automatically since headers are // only used in request mode. const result = validateCmcdHeaders({ "CMCD-Object": "br=5000,d=4000,ot=v", "CMCD-Request": "bl=25000", "CMCD-Session": 'sid="abc",sf=d,st=v', }); if (!result.valid) { // Issues may include shard placement errors and/or payload validation errors console.error("Validation issues:", result.issues); } ``` ## Version-Aware Validation The validators detect the CMCD version from the `v` key in the payload. If `v` is absent, they assume version 1, per the specification. The `version` option overrides the detection. ```typescript import { validateCmcd } from "@svta/cml-cmcd"; // Version inferred from payload (v=2) const v2Result = validateCmcd({ v: 2, br: [5000], sid: "abc" }); // Version explicitly set via options const v1Result = validateCmcd({ br: 5000, sid: "abc" }, { version: 1 }); ``` ### Version-Specific Behavior - **Key validation**: v2-only keys, such as `sta`, `ec`, and `ab`, are rejected when validating as v1 - **Type validation**: Some keys have different types between versions. For example, `bl` is an integer in v1 but an inner list (array) in v2. Each element of an inner list must be an integer - **Rounding**: `dl`, `mtp`, and `rtp` must be multiples of 100 in both versions. `bl` must be a multiple of 100 in v1. A value that violates one of these rules is an error. In v2, a `bl` or `tbl` value that is not a multiple of 100 produces a warning - **Version key**: v2 payloads must include the `v` key. v1 payloads should omit it - **Key conflicts**: In v2, `d` and `tpb` must match the object type in `ot`. `d` is allowed for `a`, `v`, `av`, `tt`, `c`, and `o`. `tpb` is allowed for `a`, `v`, `av`, and `c`. `ab`, `lab`, and `tab` must not appear together with `br`, `lb`, and `tb`. Each conflict is an error ## Handling Validation Results ### Errors and Warnings A payload is `valid` if it has zero errors. Warnings mark specification recommendations that are not strict violations. ```typescript import { validateCmcd, CMCD_VALIDATION_SEVERITY_ERROR } from "@svta/cml-cmcd"; const result = validateCmcd({ v: 2, bl: [150], sid: "abc" }); // Separate errors from warnings const errors = result.issues.filter( (i) => i.severity === CMCD_VALIDATION_SEVERITY_ERROR, ); const warnings = result.issues.filter( (i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR, ); console.log(`Valid: ${result.valid}`); console.log(`Errors: ${errors.length}, Warnings: ${warnings.length}`); // bl=150 produces a warning (should be rounded to the nearest 100 ms) // but the payload is still valid ``` ### Logging Issues ```typescript import { validateCmcdEvents } from "@svta/cml-cmcd"; function validateAndLog(body: string): boolean { const result = validateCmcdEvents(body); for (const issue of result.issues) { const level = issue.severity === "error" ? "ERROR" : "WARN"; const key = issue.key ? `[${issue.key}] ` : ""; console.log(`${level}: ${key}${issue.message}`); } return result.valid; } ``` ## Custom Keys All validators recognize custom keys: lowercase hyphenated keys that `isCmcdCustomKey` accepts, such as `com.example-mykey`. Keys that fail the check, including names with uppercase letters or a leading digit, are reported as unknown keys. For sending custom keys through `CmcdReporter`, including the naming rules and the `enabledKeys` requirements, see the [User Guide](https://cml.svta.org/llms/cmcd/docs/user-guide.md#custom-keys). - `validateCmcdKeys`: accepts custom keys without error - `validateCmcdValues`: checks that custom key values are strings of at most 64 characters, and that the key name contains a hyphen - `validateCmcdHeaders`: allows custom keys in any header shard ```typescript import { validateCmcdValues } from "@svta/cml-cmcd"; const result = validateCmcdValues({ "com.example-debug": "enabled", "x-player-version": "2.1.0", }); // result.valid === true ``` ## References - [CTA-5004-B: Common Media Client Data v2](https://cta-wave.github.io/Resources/common-media-client-data--cta-5004-b.html) - [CTA-5004: Common Media Client Data v1 (PDF)](https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf) ## API Report File for "@svta/cml-cmcd" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { DeepReadonly } from '@svta/cml-utils'; import { ExclusiveRecord } from '@svta/cml-utils'; import { HttpRequest } from '@svta/cml-utils'; import { HttpResponse } from '@svta/cml-utils'; import { SfBareItem } from '@svta/cml-structured-field-values'; import { SfItem } from '@svta/cml-structured-field-values'; import { SfToken } from '@svta/cml-structured-field-values'; import { ValueOf } from '@svta/cml-utils'; import { ValueOrArray } from '@svta/cml-utils'; // @public export function appendCmcdHeaders(headers: Record, cmcd: Cmcd, options?: CmcdEncodeOptions): Record; // @public export function appendCmcdQuery(url: string, cmcd: Cmcd, options?: CmcdEncodeOptions): string; // @public export type Cmcd = CmcdRequest & CmcdResponse & CmcdEvent; // @public export const CMCD_DEFAULT_TIME_INTERVAL = 30; // @public export const CMCD_EVENT_AD_BREAK_END: "abe"; // @public export const CMCD_EVENT_AD_BREAK_START: "abs"; // @public export const CMCD_EVENT_AD_END: "ae"; // @public export const CMCD_EVENT_AD_START: "as"; // @public export const CMCD_EVENT_BACKGROUNDED_MODE: "b"; // @public export const CMCD_EVENT_BITRATE_CHANGE: "bc"; // @public export const CMCD_EVENT_CONTENT_ID: "c"; // @public export const CMCD_EVENT_CUSTOM_EVENT: "ce"; // @public export const CMCD_EVENT_ERROR: "e"; // @public export const CMCD_EVENT_KEYS: readonly ["cen", "e", "h", "ts"]; // @public export const CMCD_EVENT_MODE: "event"; // @public export const CMCD_EVENT_MUTE: "m"; // @public export const CMCD_EVENT_PLAY_STATE: "ps"; // @public export const CMCD_EVENT_PLAYBACK_RATE: "pr"; // @public export const CMCD_EVENT_PLAYER_COLLAPSE: "pc"; // @public export const CMCD_EVENT_PLAYER_EXPAND: "pe"; // @public export const CMCD_EVENT_RESPONSE_RECEIVED: "rr"; // @public export const CMCD_EVENT_SKIP: "sk"; // @public export const CMCD_EVENT_TIME_INTERVAL: "t"; // @public export const CMCD_EVENT_UNMUTE: "um"; // @public export const CMCD_FORMATTER_MAP: Record; // @public export const CMCD_HEADER_FIELDS: readonly CmcdHeaderField[]; // @public export const CMCD_HEADER_MAP: Record; // @public export const CMCD_HEADERS: "headers"; // @public @deprecated export const CMCD_JSON: "json"; // @public export const CMCD_KEYS: CmcdKey[]; // @public export const CMCD_MIME_TYPE = "application/cmcd"; // @public export const CMCD_OBJECT: "CMCD-Object"; // @public export const CMCD_PARAM = "CMCD"; // @public export const CMCD_QUERY: "query"; // @public export const CMCD_RECORDED_REPORT_MODE_EVENT: "event"; // @public export const CMCD_RECORDED_REPORT_MODE_HEADER: "header"; // @public export const CMCD_RECORDED_REPORT_MODE_QUERY: "query"; // @public export const CMCD_RECORDED_REQUEST_TYPE_EVENT: "event"; // @public export const CMCD_RECORDED_REQUEST_TYPE_MANIFEST: "manifest"; // @public export const CMCD_RECORDED_REQUEST_TYPE_SEGMENT: "segment"; // @public export const CMCD_RECORDED_REQUEST_TYPE_UNKNOWN: "unknown"; // @public export const CMCD_REQUEST: "CMCD-Request"; // @public export const CMCD_REQUEST_KEYS: readonly ["ab", "bg", "bl", "br", "bs", "bsa", "bsd", "bsda", "cdn", "cid", "cs", "d", "dfa", "dl", "ec", "lab", "lb", "ltc", "msd", "mtp", "nor", "nr", "ot", "pb", "pr", "pt", "rtp", "sf", "sid", "sn", "st", "sta", "su", "tab", "tb", "tbl", "tpb", "v"]; // @public export const CMCD_REQUEST_MODE: "request"; // @public export const CMCD_REQUEST_PROVENANCE: unique symbol; // @public export const CMCD_RESPONSE_KEYS: readonly ["cmsdd", "cmsds", "rc", "smrt", "ttfb", "ttfbb", "ttlb", "url"]; // @public export const CMCD_SESSION: "CMCD-Session"; // @public export const CMCD_STATUS: "CMCD-Status"; // @public export const CMCD_V1: 1; // @public export const CMCD_V1_KEYS: readonly ["bl", "br", "bs", "cid", "d", "dl", "mtp", "nor", "nrr", "ot", "pr", "rtp", "sf", "sid", "st", "su", "tb", "v"]; // @public export const CMCD_V2: 2; // @public export const CMCD_VALIDATION_SEVERITY_ERROR: "error"; // @public export const CMCD_VALIDATION_SEVERITY_WARNING: "warning"; // @public export type CmcdCustomKey = Lowercase<`${string}-${string}`>; // @public export type CmcdCustomValue = string | SfItem | (string | SfItem)[] | number | SfItem | (number | SfItem)[] | boolean | SfItem | (boolean | SfItem)[] | symbol | SfItem | (symbol | SfItem)[] | SfToken | SfItem | (SfToken | SfItem)[]; // @public export type CmcdData = CmcdV1Data | CmcdV2Data; // @public export type CmcdDataValidationResult = CmcdValidationResult & { data: CmcdData; }; // @public export type CmcdDecodeOptions = { convertToLatest?: boolean; useSymbol?: boolean; }; // @public export type CmcdEncodeOptions = { version?: CmcdVersion; reportingMode?: CmcdReportingMode; formatters?: Partial; customHeaderMap?: Partial; filter?: (key: CmcdKey) => boolean; baseUrl?: string; events?: string[]; }; // @public export type CmcdEvent = CmcdRequest & { cen?: string; e?: CmcdEventType; h?: string; ts?: number; }; // @public export type CmcdEventReportConfig> = CmcdReportConfig & { version?: typeof CMCD_V2; url: string; events?: CmcdEventType[]; interval?: number; batchSize?: number; transform?: CmcdEventReportTransform; }; // @public export type CmcdEventReportTransform> = (data: Cmcd, request: CmcdTransformRequest | undefined) => Cmcd | null; // @public export type CmcdEventsValidationResult = CmcdValidationResult & { data: CmcdData[]; }; // @public export const CmcdEventType: { readonly BITRATE_CHANGE: typeof CMCD_EVENT_BITRATE_CHANGE; readonly PLAY_STATE: typeof CMCD_EVENT_PLAY_STATE; readonly PLAYBACK_RATE: typeof CMCD_EVENT_PLAYBACK_RATE; readonly ERROR: typeof CMCD_EVENT_ERROR; readonly TIME_INTERVAL: typeof CMCD_EVENT_TIME_INTERVAL; readonly CONTENT_ID: typeof CMCD_EVENT_CONTENT_ID; readonly BACKGROUNDED_MODE: typeof CMCD_EVENT_BACKGROUNDED_MODE; readonly MUTE: typeof CMCD_EVENT_MUTE; readonly UNMUTE: typeof CMCD_EVENT_UNMUTE; readonly PLAYER_EXPAND: typeof CMCD_EVENT_PLAYER_EXPAND; readonly PLAYER_COLLAPSE: typeof CMCD_EVENT_PLAYER_COLLAPSE; readonly RESPONSE_RECEIVED: typeof CMCD_EVENT_RESPONSE_RECEIVED; readonly AD_START: typeof CMCD_EVENT_AD_START; readonly AD_END: typeof CMCD_EVENT_AD_END; readonly AD_BREAK_START: typeof CMCD_EVENT_AD_BREAK_START; readonly AD_BREAK_END: typeof CMCD_EVENT_AD_BREAK_END; readonly SKIP: typeof CMCD_EVENT_SKIP; readonly CUSTOM_EVENT: typeof CMCD_EVENT_CUSTOM_EVENT; }; // @public (undocumented) export type CmcdEventType = ValueOf; // @public export type CmcdFormatter = (value: CmcdValue, options: CmcdFormatterOptions) => ValueOrArray> | ValueOrArray>; // @public export type CmcdFormatterMap = Record; // @public export type CmcdFormatterOptions = { version: CmcdVersion; reportingMode: CmcdReportingMode; baseUrl?: string; }; // @public export const CmcdHeaderField: { readonly OBJECT: typeof CMCD_OBJECT; readonly REQUEST: typeof CMCD_REQUEST; readonly SESSION: typeof CMCD_SESSION; readonly STATUS: typeof CMCD_STATUS; }; // @public (undocumented) export type CmcdHeaderField = ValueOf; // @public export type CmcdHeaderKey = keyof typeof CMCD_HEADER_MAP; // @public export type CmcdHeaderMap = Record; // @public export type CmcdHeaderValue = CmcdRequest | CmcdV1; // @public export type CmcdKey = keyof Cmcd | keyof CmcdV1; // @public export const CmcdObjectType: { readonly MANIFEST: "m"; readonly AUDIO: "a"; readonly VIDEO: "v"; readonly MUXED: "av"; readonly INIT: "i"; readonly CAPTION: "c"; readonly TIMED_TEXT: "tt"; readonly KEY: "k"; readonly OTHER: "o"; }; // @public (undocumented) export type CmcdObjectType = ValueOf; // @public export type CmcdObjectTypeList = (number | SfItem>)[]; // @public export const CmcdPlayerState: { readonly STARTING: "s"; readonly PLAYING: "p"; readonly SEEKING: "k"; readonly REBUFFERING: "r"; readonly PAUSED: "a"; readonly WAITING: "w"; readonly ENDED: "e"; readonly FATAL_ERROR: "f"; readonly QUIT: "q"; readonly PRELOADING: "d"; }; // @public (undocumented) export type CmcdPlayerState = ValueOf; // @public export type CmcdRecordedReport = { readonly request: HttpRequest; readonly type: CmcdRecordedRequestType; readonly reportingMode: CmcdRecordedReportMode; readonly timestamp: number; }; // @public export const CmcdRecordedReportMode: { readonly QUERY: typeof CMCD_RECORDED_REPORT_MODE_QUERY; readonly HEADER: typeof CMCD_RECORDED_REPORT_MODE_HEADER; readonly EVENT: typeof CMCD_RECORDED_REPORT_MODE_EVENT; }; // @public (undocumented) export type CmcdRecordedReportMode = ValueOf; // @public export const CmcdRecordedRequestType: { readonly MANIFEST: typeof CMCD_RECORDED_REQUEST_TYPE_MANIFEST; readonly SEGMENT: typeof CMCD_RECORDED_REQUEST_TYPE_SEGMENT; readonly EVENT: typeof CMCD_RECORDED_REQUEST_TYPE_EVENT; readonly UNKNOWN: typeof CMCD_RECORDED_REQUEST_TYPE_UNKNOWN; }; // @public (undocumented) export type CmcdRecordedRequestType = ValueOf; // @public export type CmcdReportConfig = { version?: CmcdVersion; enabledKeys?: CmcdKey[]; }; // @public export class CmcdReporter> { constructor(config: Partial>, requester?: (request: HttpRequest) => Promise<{ status: number; }>); // @deprecated applyRequestReport(req: HttpRequest): HttpRequest; createRequestReport> = HttpRequest>(request: R, data?: Partial): R & CmcdRequestReport; flush(): void; isRequestReportingEnabled(): boolean; recordEvent(type: CmcdEventType, data?: Partial): void; recordResponseReceived = C>(response: HttpResponse>, data?: Partial): void; start(): void; stop(flush?: boolean): void; update(data: Partial): void; } // @public export type CmcdReporterConfig> = CmcdRequestReportConfig & { sid?: string; cid?: string; eventTargets?: CmcdEventReportConfig[]; sessionRetention?: number; }; // @public export type CmcdReporterCustomData = Record extends C ? any : C; // @public export const CmcdReportingMode: { readonly REQUEST: typeof CMCD_REQUEST_MODE; readonly EVENT: typeof CMCD_EVENT_MODE; }; // @public (undocumented) export type CmcdReportingMode = ValueOf; // @public export class CmcdReportRecorder { attach(options?: CmcdReportRecorderOptions): void; clear(): void; detach(): void; getReports(): CmcdRecordedReport[]; waitForEvents(options?: CmcdReportRecorderWaitOptions): Promise; waitForManifest(options?: CmcdReportRecorderWaitOptions): Promise; waitForReports(options?: CmcdReportRecorderWaitOptions): Promise; waitForSegments(options?: CmcdReportRecorderWaitOptions): Promise; } // @public export type CmcdReportRecorderOptions = { eventTargetUrls?: readonly string[]; transports?: readonly CmcdTransportAdapter[]; waitTimeout?: number; onReport?: (report: CmcdRecordedReport) => void; }; // @public export type CmcdReportRecorderWaitOptions = { count?: number; timeout?: number; }; // @public export type CmcdRequest = { [index: CmcdCustomKey]: CmcdCustomValue | undefined; ab?: CmcdObjectTypeList; bg?: boolean; bl?: CmcdObjectTypeList; br?: CmcdObjectTypeList; bs?: boolean; bsa?: CmcdObjectTypeList; bsd?: CmcdObjectTypeList; bsda?: CmcdObjectTypeList; cdn?: string; cid?: string; cs?: string; d?: number; dfa?: number; dl?: number; ec?: string[]; lab?: CmcdObjectTypeList; lb?: CmcdObjectTypeList; ltc?: number; msd?: number; mtp?: CmcdObjectTypeList; nor?: (string | SfItem)[]; nr?: boolean; ot?: CmcdObjectType; pb?: CmcdObjectTypeList; pr?: number; pt?: number; rtp?: number; sf?: CmcdStreamingFormat; sid?: string; sn?: number; st?: CmcdStreamType; sta?: CmcdPlayerState; su?: boolean; tab?: CmcdObjectTypeList; tb?: CmcdObjectTypeList; tbl?: CmcdObjectTypeList; tpb?: CmcdObjectTypeList; v?: number; }; // @public export type CmcdRequestDeliver = (request: HttpRequest) => Response | undefined; // @public export type CmcdRequestKey = keyof CmcdRequest | "nrr"; // @public export type CmcdRequestProvenance = { readonly sid: string; readonly cid?: string; readonly data?: string; }; // @public export type CmcdRequestReport = HttpRequest & { customData: { cmcd: Cmcd; [CMCD_REQUEST_PROVENANCE]?: CmcdRequestProvenance; } & D; headers: Record; }; // @public export type CmcdRequestReportConfig> = CmcdReportConfig & { transmissionMode?: CmcdTransmissionMode; customHeaderMap?: Partial; transform?: CmcdRequestReportTransform; }; // @public export type CmcdRequestReportTransform> = (data: Cmcd, request: CmcdTransformRequest) => Cmcd | null; // @public export type CmcdResponse = CmcdRequest & { cmsdd?: string; cmsds?: string; rc?: number; smrt?: string; ttfb?: number; ttfbb?: number; ttlb?: number; url?: string; }; // @public export const CmcdStreamingFormat: { readonly DASH: "d"; readonly HLS: "h"; readonly SMOOTH: "s"; readonly OTHER: "o"; }; // @public (undocumented) export type CmcdStreamingFormat = ValueOf; // @public export const CmcdStreamType: { readonly VOD: "v"; readonly LIVE: "l"; readonly LOW_LATENCY: "ll"; }; // @public (undocumented) export type CmcdStreamType = ValueOf; // @public export type CmcdTransformRequest> = Readonly> & { readonly headers?: Readonly>; readonly customData?: DeepReadonly; }; // @public export const CmcdTransmissionMode: { readonly JSON: typeof CMCD_JSON; readonly QUERY: typeof CMCD_QUERY; readonly HEADERS: typeof CMCD_HEADERS; }; // @public (undocumented) export type CmcdTransmissionMode = ValueOf; // @public export type CmcdTransportAdapter = { attach(deliver: CmcdRequestDeliver): () => void; }; // @public export type CmcdV1 = { bl?: number; br?: number; mtp?: number; nor?: string; nrr?: string; tb?: number; }; // @public export type CmcdV1Data = Omit & CmcdV1 & { v?: 1; }; // @public export type CmcdV2Data = Omit & { v: 2; }; // @public export type CmcdValidationIssue = { key?: string; message: string; severity: CmcdValidationSeverity; }; // @public export type CmcdValidationOptions = { version?: CmcdVersion; reportingMode?: CmcdReportingMode; }; // @public export type CmcdValidationResult = { valid: boolean; issues: CmcdValidationIssue[]; }; // @public export const CmcdValidationSeverity: { readonly ERROR: typeof CMCD_VALIDATION_SEVERITY_ERROR; readonly WARNING: typeof CMCD_VALIDATION_SEVERITY_WARNING; }; // @public export type CmcdValidationSeverity = ValueOf; // @public export type CmcdValue = ValueOf; // @public export type CmcdVersion = typeof CMCD_V1 | typeof CMCD_V2; // @public export function createFetchTransport(): CmcdTransportAdapter; // @public export function createXhrTransport(): CmcdTransportAdapter; // @public export function decodeCmcd(cmcd: string, options: CmcdDecodeOptions & { convertToLatest: true; }): Cmcd; // @public export function decodeCmcd(cmcd: string, options?: CmcdDecodeOptions): T; // @public export function encodeCmcd(cmcd: Cmcd, options?: CmcdEncodeOptions): string; // @public export function fromCmcdHeaders(headers: Record | Headers, options: CmcdDecodeOptions & { convertToLatest: true; }): Cmcd; // @public export function fromCmcdHeaders(headers: Record | Headers, options?: CmcdDecodeOptions): CmcdData; // @public export function fromCmcdQuery(query: string | URLSearchParams, options: CmcdDecodeOptions & { convertToLatest: true; }): Cmcd; // @public export function fromCmcdQuery(query: string | URLSearchParams, options?: CmcdDecodeOptions): CmcdData; // @public export function fromCmcdUrl(url: string, options: CmcdDecodeOptions & { convertToLatest: true; }): Cmcd; // @public export function fromCmcdUrl(url: string, options?: CmcdDecodeOptions): CmcdData; // @public export function groupCmcdHeaders(cmcd: Cmcd, customHeaderMap?: Partial): Record; // @public export function isCmcdCustomKey(key: CmcdKey): boolean; // @public export function isCmcdEventKey(key: string): key is keyof Cmcd; // @public export function isCmcdRequestKey(key: string): key is keyof Cmcd; // @public export function isCmcdResponseReceivedKey(key: string): key is keyof Cmcd; // @public export function isCmcdV1Data(data: CmcdData): data is CmcdV1Data; // @public export function isCmcdV1Key(key: string): key is CmcdKey; // @public export function isCmcdV2Data(data: CmcdData): data is CmcdV2Data; // @public export function prepareCmcdData(obj: Record, options?: CmcdEncodeOptions): Cmcd; // @public export function toCmcdHeaders(cmcd: Cmcd, options?: CmcdEncodeOptions): Record; // @public export function toCmcdQuery(cmcd: Cmcd, options?: CmcdEncodeOptions): string; // @public export function toCmcdUrl(cmcd: Cmcd, options?: CmcdEncodeOptions): string; // @public export function toCmcdValue(value: V, params?: P): SfItem; // @public export function validateCmcd(data: Record, options?: CmcdValidationOptions): CmcdValidationResult; // @public export function validateCmcdEventReport(request: HttpRequest, options?: Omit): CmcdEventsValidationResult; // @public export function validateCmcdEvents(cmcd: string, options?: Omit): CmcdEventsValidationResult; // @public export function validateCmcdHeaders(headers: Record | Headers, options?: Omit): CmcdDataValidationResult; // @public export function validateCmcdKeys(data: Record, options?: CmcdValidationOptions): CmcdValidationResult; // @public export function validateCmcdRequest(request: Request | HttpRequest, options?: Omit): CmcdDataValidationResult; // @public export function validateCmcdStructure(data: Record, options?: CmcdValidationOptions): CmcdValidationResult; // @public export function validateCmcdValues(data: Record, options?: CmcdValidationOptions): CmcdValidationResult; // (No @packageDocumentation comment for this package) ``` # @svta/cml-cmsd Common Media Server Data (CMSD) encoding and decoding. ## When to use - Decode the CMSD-Static and CMSD-Dynamic response headers: `decodeCmsdStatic`, `decodeCmsdDynamic` - Encode CMSD headers on a server or an edge: `encodeCmsdStatic`, `encodeCmsdDynamic` - Type the CMSD keys and values that a player reads: `CmsdStatic`, `CmsdDynamic`, `CmsdObjectType` - Distinguish a custom key from a standard key: `isCmsdCustomKey` Do not write your own CMSD header parser or serializer. Open an issue for a key that is missing. ## Installation ```bash npm i @svta/cml-cmsd ``` ## Usage ```typescript import { CmsdObjectType, CmsdStreamingFormat, CmsdStreamType, encodeCmsdStatic, } from "@svta/cml-cmsd"; import type { CmsdStatic } from "@svta/cml-cmsd"; const input: CmsdStatic = { ot: CmsdObjectType.VIDEO, sf: CmsdStreamingFormat.HLS, st: CmsdStreamType.VOD, d: 5000, br: 2000, n: "OriginProviderA", }; console.log(encodeCmsdStatic(input)); // ot=v,sf=h,st=v,d=5000,br=2000,n="OriginProviderA" ``` ## API Report File for "@svta/cml-cmsd" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { CmObjectType } from '@svta/cml-cta'; import { CmStreamingFormat } from '@svta/cml-cta'; import { CmStreamType } from '@svta/cml-cta'; import { SfItem } from '@svta/cml-structured-field-values'; import { SfToken } from '@svta/cml-structured-field-values'; import { ValueOf } from '@svta/cml-utils'; // @public export const CMSD_DYNAMIC = "CMSD-Dynamic"; // @public export const CMSD_STATIC = "CMSD-Static"; // @public export const CMSD_V1 = 1; // @public export type CmsdCustomKey = Lowercase<`${string}-${string}`>; // @public export type CmsdDynamic = { value: string; params: CmsdDynamicParams; }; // @public export type CmsdDynamicParams = { [index: CmsdCustomKey]: CmsdValue; du?: boolean; etp?: number; mb?: number; rd?: number; rtt?: number; }; // @public export type CmsdEncodeOptions = { useSymbol?: boolean; }; // @public export const CmsdHeaderField: { readonly STATIC: typeof CMSD_STATIC; readonly DYNAMIC: typeof CMSD_DYNAMIC; }; // @public (undocumented) export type CmsdHeaderField = ValueOf; // @public export const CmsdObjectType: typeof CmObjectType; // @public (undocumented) export type CmsdObjectType = CmObjectType; // @public export type CmsdStatic = { [index: CmsdCustomKey]: CmsdValue; at?: number; br?: number; d?: number; ht?: number; n?: string; nor?: string; nrr?: string; ot?: CmsdObjectType; sf?: CmsdStreamingFormat; st?: CmsdStreamType; su?: boolean; v?: number; }; // @public export const CmsdStreamingFormat: typeof CmStreamingFormat; // @public (undocumented) export type CmsdStreamingFormat = CmStreamingFormat; // @public export const CmsdStreamType: typeof CmStreamType; // @public (undocumented) export type CmsdStreamType = CmStreamType; // @public export type CmsdValue = string | number | boolean | symbol | SfToken; // @public export function decodeCmsdDynamic(cmsd: string): CmsdDynamic[]; // @public export function decodeCmsdStatic(cmsd: string): CmsdStatic; // @public export function encodeCmsdDynamic(value: SfItem[]): string; // @public export function encodeCmsdDynamic(value: string, cmsd: CmsdDynamic): string; // @public export function encodeCmsdStatic(cmsd: CmsdStatic, options?: CmsdEncodeOptions): string; // @public export function isCmsdCustomKey(key: string): boolean; // (No @packageDocumentation comment for this package) ``` # @svta/cml-content-steering Content steering functionality. ## When to use - Validate a content steering manifest before the player applies it: `isValidSteeringManifest` - Validate a pathway clone and its URI replacement: `isValidPathwayClone` - Type the steering manifest, the pathway clones, and the URI replacements: `SteeringManifest`, `PathwayClone`, `UriReplacement` - Apply the default TTL and pathway penalty of the specification: `DEFAULT_TTL`, `DEFAULT_PATHWAY_PENALTY` Do not write your own steering manifest validation or types. Open an issue for a field that is missing. ## Installation ```bash npm i @svta/cml-content-steering ``` ## Usage ```typescript import { isValidPathwayClone, isValidSteeringManifest, } from "@svta/cml-content-steering"; const manifest = { VERSION: 1, TTL: 100, "PATHWAY-PRIORITY": ["pathway1", "pathway2"], "PATHWAY-CLONES": [ { "BASE-ID": "pathway1", ID: "clone1", "URI-REPLACEMENT": { HOST: "example.com", PARAMS: { param1: "value1", param2: "value2", }, }, }, ], }; const pathwayClone = { "BASE-ID": "pathway1", ID: "clone1", "URI-REPLACEMENT": { HOST: "example.com", PARAMS: { param1: "value1", param2: "value2", }, }, }; const isValidManifest = isValidSteeringManifest(manifest); const isValidClone = isValidPathwayClone(pathwayClone); ``` ## API Report File for "@svta/cml-content-steering" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @beta export const DEFAULT_PATHWAY_PENALTY = 3e5; // @beta export const DEFAULT_TTL = 300; // @beta export function isValidPathwayClone(clone: PathwayClone): boolean; // @beta export function isValidSteeringManifest(manifest: SteeringManifest): boolean; // @beta export type PathwayClone = { "BASE-ID": string; ID: string; "URI-REPLACEMENT": UriReplacement; }; // @beta export type SteeringManifest = { VERSION: number; TTL: number; "RELOAD-URI"?: string; "PATHWAY-PRIORITY": string[]; "PATHWAY-CLONES"?: PathwayClone[]; }; // @beta export type UriReplacement = { HOST?: string; PARAMS?: Record; }; // (No @packageDocumentation comment for this package) ``` # @svta/cml-cta Common functionality for CTA standards, including CMCD, CMSD, and CTA-608/CEA-608. Do not use this utility package directly. Use `@svta/cml-cmcd`, `@svta/cml-cmsd`, or `@svta/cml-608`. ## When to use - Type a custom key with a hyphenated prefix for CMCD or CMSD data: `CmCustomKey` Do not import this package into a player directly. Use `@svta/cml-cmcd` or `@svta/cml-cmsd`. ## Installation ```bash npm i @svta/cml-cta ``` ## Usage No direct usage. See the packages listed above. ## API Report File for "@svta/cml-cta" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { SfItem } from '@svta/cml-structured-field-values'; import { SfToken } from '@svta/cml-structured-field-values'; import { ValueOf } from '@svta/cml-utils'; // @public export type CmCustomKey = `${string}-${string}`; // @internal export const CmObjectType: { readonly MANIFEST: "m"; readonly AUDIO: "a"; readonly VIDEO: "v"; readonly MUXED: "av"; readonly INIT: "i"; readonly CAPTION: "c"; readonly TIMED_TEXT: "tt"; readonly KEY: "k"; readonly OTHER: "o"; }; // @internal export type CmObjectType = ValueOf; // @internal export const CmStreamingFormat: { readonly DASH: "d"; readonly HLS: "h"; readonly SMOOTH: "s"; readonly OTHER: "o"; }; // @internal export type CmStreamingFormat = ValueOf; // @internal export const CmStreamType: { readonly VOD: "v"; readonly LIVE: "l"; }; // @internal export type CmStreamType = ValueOf; // @internal export type CmValue = string | string[] | number | number[] | boolean | symbol | SfToken | SfItem | SfItem[]; // @internal export function isTokenField(key: string): boolean; // @internal export function isValid(value: CmValue): boolean; // (No @packageDocumentation comment for this package) ``` # @svta/cml-dash DASH manifest parsing functionality. ## When to use - Expand a DASH SegmentTemplate URL with the $RepresentationID$, $Number$, $Bandwidth$, and $Time$ identifiers and their format tags: `processUriTemplate` - Parse a DASH frame rate string such as 24000/1001: `parseFrameRate` - Detect the DASH-IF thumbnail tile scheme on an EssentialProperty: `THUMBNAIL_TILE_SCHEME_ID_URI` Do not write your own template expansion or frame rate parser. Open an issue for a DASH utility that is missing. ## Installation ```bash npm i @svta/cml-dash ``` ## Usage ```typescript import { processUriTemplate, parseFrameRate } from "@svta/cml-dash"; // Process URI template const result = processUriTemplate( "http://example.com/$RepresentationID$/$Number$/$SubNumber$/$Bandwidth$/$Time%02d$/$$", "rep1", // RepresentationID 1, // Number 2, // SubNumber 3, // Bandwidth 4 // Time ); console.log(result); // http://example.com/rep1/1/2/3/04/$ // Parse frame rate const frameRate = parseFrameRate("24000/1001"); console.log(frameRate); // 23.976023976023978 ``` ## API Report File for "@svta/cml-dash" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @public export function parseFrameRate(frameRate: string): number; // @public export function processUriTemplate(uriTemplate: string, representationId: string | null | undefined, number: number | null | undefined, subNumber: number | null | undefined, bandwidth: number | null | undefined, time: string | number | null | undefined): string; // @public export const THUMBNAIL_TILE_SCHEME_ID_URI = "http://dashif.org/guidelines/thumbnail_tile"; // (No @packageDocumentation comment for this package) ``` # @svta/cml-drm Digital Rights Management (DRM) functionality. ## When to use - Parse the pssh boxes in init data and find the one for a key system: `parsePsshList`, `getPsshForKeySystem`, `getPsshData` - Request key system access with a configuration built from codecs and robustness levels: `getKeySystemAccess`, `createMediaKeySystemConfiguration`, `getSupportedKeySystemConfiguration` - Read the license request and its headers from an EME key message: `getLicenseRequestFromMessage`, `getRequestHeadersFromMessage` - Read the license server URL from a ContentProtection element or FairPlay init data: `getLicenseServerUrl`, `getLicenseServerUrlFromContentProtection` - Build FairPlay init data and decode a FairPlay license: `concatInitDataIdAndCertificate`, `extractContentId`, `decodeFairPlayLicense` - Name key systems, UUIDs, encryption schemes, and robustness levels: `WIDEVINE_KEY_SYSTEM`, `PLAYREADY_UUID`, `EncryptionScheme`, `HW_SECURE_ALL` Do not write your own pssh parser, key message decoder, or key system constant table. Open an issue for a key system or a message format that is missing. ## Installation ```bash npm i @svta/cml-drm ``` ## Usage ```typescript import { parsePsshList, W3C_CLEAR_KEY_UUID } from "@svta/cml-drm"; // A minimal pssh box: size, type, version and flags, system ID, data size. const initData = new Uint8Array([ 0x00, 0x00, 0x00, 0x20, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, 0x00, 0x00, ]); const psshList = parsePsshList(initData); console.log(Object.keys(psshList)); // [ '1077efec-c0b2-4d02-ace3-3c1e52e2fb4b' ] console.log(Object.keys(psshList)[0] === W3C_CLEAR_KEY_UUID); // true ``` ## API Report File for "@svta/cml-drm" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { Encoding } from '@svta/cml-utils'; import { UTF_16 } from '@svta/cml-utils'; import { UTF_8 } from '@svta/cml-utils'; import { ValueOf } from '@svta/cml-utils'; // @public export const CBCS = "cbcs"; // @public export const CENC = "cenc"; // @public export const CHALLENGE = "Challenge"; // @public export const CLEAR_KEY_SYSTEM = "org.w3.clearkey"; // @public export const CLEAR_KEY_UUID = "e2719d58-a985-b3c9-781a-b030af78d30e"; // @public export function concatInitDataIdAndCertificate(initData: Uint16Array, id: Uint16Array | string, cert: Uint8Array): Uint8Array; // @public export const CONTENT_TYPE = "Content-Type"; // @public export type ContentProtection = { schemeIdUri?: string; value?: string; pssh?: string; laUrl?: string; }; // @public export function createMediaKeySystemConfiguration(supportedAudio: MediaKeySystemMediaCapability[] | null, supportedVideo: MediaKeySystemMediaCapability[] | null): MediaKeySystemConfiguration; // @public export function decodeFairPlayLicense(response: string | ArrayBuffer): Uint8Array; // @public export const EncryptionScheme: { readonly CENC: typeof CENC; readonly CBCS: typeof CBCS; }; // @public export type EncryptionScheme = ValueOf; // @public export function ensureEncryptedInit(init: Uint8Array, options?: Partial): Uint8Array; // @public export type EnsureEncryptedInitOptions = { prepend: boolean; includeOriginal: boolean; schemeType: number; defaultKid: number[]; }; // @public export const EXPIRED = "expired"; // @public export function extractContentId(initData: ArrayBufferLike, encoding?: Encoding): string; // @public export const FAIRPLAY_KEY_SYSTEM = "com.apple.fps.1_0"; // @public export const FAIRPLAY_UUID = "29701fe4-3cc7-4a34-8c5b-ae90c7439a47"; // @public export function findCencContentProtection(cpArray: ContentProtection[]): ContentProtection | null; // @public export function getId(licenseServerUrl: string, initData: ArrayBufferLike, queryParam?: string): string; // @public export function getKeySystemAccess(requests: MediaKeySystemAccessRequest[]): Promise; // @public export function getLegacyKeySystemAccess(requests: MediaKeySystemAccessRequest[]): MediaKeySystemAccessRequest | null; // @public export function getLicenseRequestFromMessage(message: ArrayBuffer, encoding?: Encoding): ArrayBuffer; // @public export function getLicenseServerUrl(initData: Uint16Array): string; // @public export function getLicenseServerUrlFromContentProtection(contentProtectionElements: ContentProtection[], schemeIdUri: string): string | null; // @public export function getPsshData(pssh: ArrayBufferLike): ArrayBufferLike; // @public export function getPsshForKeySystem(uuid: string, initData: ArrayBufferLike): ArrayBufferLike | null; // @public export function getRequestHeadersFromMessage(message: ArrayBuffer, encoding?: typeof UTF_8 | typeof UTF_16): Record; // @public export function getSupportedKeySystemConfiguration(keySystem: string, configs: Iterable): { supportedAudio: MediaKeySystemMediaCapability[]; supportedVideo: MediaKeySystemMediaCapability[]; }; // @public export const HTTP_HEADERS = "HttpHeaders"; // @public export const HW_SECURE_ALL = "HW_SECURE_ALL"; // @public export const HW_SECURE_CRYPTO = "HW_SECURE_CRYPTO"; // @public export const HW_SECURE_DECODE = "HW_SECURE_DECODE"; // @public export const INDIVIDUALIZATION_REQUEST = "individualization-request"; // @public export const InitializationDataType: { readonly CENC: typeof CENC; readonly CBCS: typeof CBCS; readonly KEYIDS: typeof KEYIDS; readonly WEBM: typeof WEBM; }; // @public export type InitializationDataType = ValueOf; // @public export const INTERNAL_ERROR = "internal-error"; // @public export const KEYIDS = "keyids"; // @public export const LICENSE_ACQUISITION = "LicenseAcquisition"; // @public export const LICENSE_RELEASE = "license-release"; // @public export const LICENSE_RENEWAL = "license-renewal"; // @public export const LICENSE_REQUEST = "license-request"; // @public export type LicenseRequest = { url: string; method: "GET" | "POST"; responseType: XMLHttpRequestResponseType; headers?: Record; withCredentials?: boolean; messageType?: MediaKeyMessageType; sessionId?: string; data?: ArrayBuffer; }; // @public const MediaKeyMessageType_2: { readonly LICENSE_REQUEST: typeof LICENSE_REQUEST; readonly LICENSE_RENEWAL: typeof LICENSE_RENEWAL; readonly LICENSE_RELEASE: typeof LICENSE_RELEASE; readonly INDIVIDUALIZATION_REQUEST: typeof INDIVIDUALIZATION_REQUEST; }; // @public type MediaKeyMessageType_2 = ValueOf; export { MediaKeyMessageType_2 as MediaKeyMessageType } // @public const MediaKeyStatus_2: { readonly USABLE: typeof USABLE; readonly EXPIRED: typeof EXPIRED; readonly RELEASED: typeof RELEASED; readonly OUTPUT_RESTRICTED: typeof OUTPUT_RESTRICTED; readonly OUTPUT_DOWNSCALED: typeof OUTPUT_DOWNSCALED; readonly STATUS_PENDING: typeof STATUS_PENDING; readonly INTERNAL_ERROR: typeof INTERNAL_ERROR; }; // @public type MediaKeyStatus_2 = ValueOf; export { MediaKeyStatus_2 as MediaKeyStatus } // @public export type MediaKeySystemAccessRequest = { keySystem: string; configurations: MediaKeySystemConfiguration[]; }; // @public export const MP4_PROTECTION_SCHEME = "urn:mpeg:dash:mp4protection:2011"; // @public export const OUTPUT_DOWNSCALED = "output-downscaled"; // @public export const OUTPUT_RESTRICTED = "output-restricted"; // @public export function parseInitDataFromContentProtection(cpData: ContentProtection, BASE64: { decodeArray: (input: string) => Uint8Array; }): ArrayBuffer | null; // @public export function parsePsshList(data: ArrayBufferLike): Record; // @public export function parsePsshList(data: Uint8Array): Record; // @public export const PLAYREADY_KEY_MESSAGE = "PlayReadyKeyMessage"; // @public export const PLAYREADY_KEY_SYSTEM = "com.microsoft.playready"; // @public export const PLAYREADY_RECOMMENDATION_KEY_SYSTEM = "com.microsoft.playready.recommendation"; // @public export const PLAYREADY_UUID = "9a04f079-9840-4286-ab92-e65be0885f95"; // @public export const RELEASED = "released"; // @public export const STATUS_PENDING = "status-pending"; // @public export const SW_SECURE_CRYPTO = "SW_SECURE_CRYPTO"; // @public export const SW_SECURE_DECODE = "SW_SECURE_DECODE"; // @public export const TEXT_XML_UTF8 = "text/xml; charset=utf-8"; // @public export function toBigEndianKeyId(keyId: ArrayBuffer): ArrayBuffer; // @public export const USABLE = "usable"; // @public export const W3C_CLEAR_KEY_UUID = "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"; // @public export const WEBM = "webm"; // @public export const WIDEVINE_KEY_SYSTEM = "com.widevine.alpha"; // @public export const WIDEVINE_UUID = "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; // @public export const WidevineRobustness: { readonly SW_SECURE_CRYPTO: typeof SW_SECURE_CRYPTO; readonly SW_SECURE_DECODE: typeof SW_SECURE_DECODE; readonly HW_SECURE_CRYPTO: typeof HW_SECURE_CRYPTO; readonly HW_SECURE_DECODE: typeof HW_SECURE_DECODE; readonly HW_SECURE_ALL: typeof HW_SECURE_ALL; }; // @public export type WidevineRobustness = ValueOf; // (No @packageDocumentation comment for this package) ``` # @svta/cml-error-codes SVTA2070 standardized streaming error codes. This package is the reference implementation of [SVTA2070: Standardized Error Codes](https://www.svta.org/product/svta2070/). The specification defines one error vocabulary for every video player. Each code is a short integer. The thousands digits identify the category, such as Playback or Network. The last three digits identify the specific error. HTTP response statuses and IAB VAST error codes embed into the scheme at their own values. ## When to use - Report a playback, network, DRM, or ad error with an SVTA2070 code: `SVTA_BUFFER_APPEND_ERROR`, `SVTA_BAD_LICENSE_REQUEST` - Map an HTTP response status to an SVTA2070 code: `httpStatusToSvtaErrorCode` - Map an IAB VAST error code to an SVTA2070 code: `vastErrorToSvtaErrorCode` - Read the category or the index of a code: `getSvtaErrorCategory`, `getSvtaErrorIndex` Do not define your own error code list for a player or an analytics pipeline. Open an issue for a condition that the specification does not name. ## Installation ```bash npm i @svta/cml-error-codes ``` ## Usage Codes are grouped into one catalog per category. Each catalog is an `as const` object, so autocomplete lists the valid members: ```typescript import assert from "node:assert"; import { getSvtaErrorCategory, SvtaErrorCategory, SvtaPlaybackErrorCode } from "@svta/cml-error-codes"; const code = SvtaPlaybackErrorCode.VIDEO_BUFFER_UNDERRUN; assert(code === 2001); assert(getSvtaErrorCategory(code) === SvtaErrorCategory.PLAYBACK); ``` Every code is also an individual constant: ```typescript import assert from "node:assert"; import { SVTA_RESOURCE_NOT_FOUND } from "@svta/cml-error-codes"; assert(SVTA_RESOURCE_NOT_FOUND === 3004); ``` ### Embed external error codes HTTP response statuses embed into the Network category. IAB VAST error codes embed into the Advertising category. The four-digit VAST error 1009 (empty VAST response) maps to 7999. ```typescript import assert from "node:assert"; import { httpStatusToSvtaErrorCode, vastErrorToSvtaErrorCode } from "@svta/cml-error-codes"; assert(httpStatusToSvtaErrorCode(404) === 3404); assert(vastErrorToSvtaErrorCode(301) === 7301); assert(vastErrorToSvtaErrorCode(1009) === 7999); ``` The helpers never throw. Out-of-range input returns the unknown code of the target category, 3000 or 7000. ## References - [SVTA2070: Standardized Error Codes](https://www.svta.org/product/svta2070/) - [IAB VAST 4.0 §2.3.6.3 Error Codes](https://www.iab.com/wp-content/uploads/2016/01/VAST_4-0_2016-01-21.pdf) ## API Report File for "@svta/cml-error-codes" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ValueOf } from '@svta/cml-utils'; // @public export function getSvtaErrorCategory(code: number): SvtaErrorCategory | undefined; // @public export function getSvtaErrorIndex(code: number): number | undefined; // @public export function httpStatusToSvtaErrorCode(status: number): number; // @public export const SVTA_ACCESSIBILITY_UNKNOWN: 5000; // @public export const SVTA_AD_BLOCKER_DETECTED: 7001; // @public export const SVTA_AD_TYPE_MISMATCH: 7200; // @public export const SVTA_ADVERTISING_UNKNOWN: 7000; // @public export const SVTA_AUDIO_BUFFER_UNDERRUN: 2002; // @public export const SVTA_AUDIO_BUFFERING_TIMEOUT: 2004; // @public export const SVTA_AUDIO_DECODE_ERROR: 2008; // @public export const SVTA_AUDIO_DESCRIPTION_EXCEEDS_CONTENT_DURATION: 5004; // @public export const SVTA_BAD_LICENSE_REQUEST: 4004; // @public export const SVTA_BUFFER_APPEND_ERROR: 2023; // @public export const SVTA_BUFFER_FULL_ERROR: 2025; // @public export const SVTA_BUFFER_INITIALIZATION_ERROR: 2022; // @public export const SVTA_BUFFER_NUDGE_ON_STALL: 2027; // @public export const SVTA_BUFFER_REMOVE_ERROR: 2024; // @public export const SVTA_BUFFER_SEEK_OVER_HOLE: 2026; // @public export const SVTA_CDN_UNAUTHORIZED: 4011; // @public export const SVTA_COMPANION_AD_ERROR: 7600; // @public export const SVTA_COMPANION_AD_FETCH_ERROR: 7603; // @public export const SVTA_COMPANION_DIMENSION_MISMATCH: 7601; // @public export const SVTA_CONCURRENT_STREAM_LIMIT_EXCEEDED: 4001; // @public export const SVTA_CONTENT_PROTECTION_UNKNOWN: 4000; // @public export const SVTA_CONTENT_STEERING_MANIFEST_LOAD_ERROR: 2040; // @public export const SVTA_CONTENT_STEERING_MANIFEST_PARSE_ERROR: 2041; // @public export const SVTA_CUSTOM_UNKNOWN: 99000; // @public export const SVTA_DOWNLOAD_ERROR: 3007; // @public export const SVTA_DRM_CERTIFICATE_ERROR: 4013; // @public export const SVTA_DRM_CONFIGURATION_MISSING: 4015; // @public export const SVTA_DRM_INITIALIZATION_ERROR: 4010; // @public export const SVTA_DRM_SESSION_ERROR: 4014; // @public export const SVTA_DURATION_MISMATCH: 7202; // @public export const SVTA_EMPTY_VAST_RESPONSE: 7999; // @public export const SVTA_EMPTY_WRAPPER_RESPONSE: 7303; // @public export const SVTA_ENTITLEMENT_REFUSED: 4002; // @public export const SVTA_ERROR_CATEGORY_ACCESSIBILITY: 5; // @public export const SVTA_ERROR_CATEGORY_ADVERTISING: 7; // @public export const SVTA_ERROR_CATEGORY_CONTENT_PROTECTION: 4; // @public export const SVTA_ERROR_CATEGORY_CUSTOM: 99; // @public export const SVTA_ERROR_CATEGORY_MEDIA_CONTENT: 1; // @public export const SVTA_ERROR_CATEGORY_NETWORK: 3; // @public export const SVTA_ERROR_CATEGORY_PLAYBACK: 2; // @public export const SVTA_ERROR_CATEGORY_REMOTE_PLAY: 6; // @public export const SVTA_ERROR_CATEGORY_UNKNOWN: 0; // @public export const SVTA_FRAGMENT_LOAD_ERROR: 2033; // @public export const SVTA_FRAGMENT_TIMEOUT_ERROR: 2034; // @public export const SVTA_FULLSCREEN_ERROR: 2037; // @public export const SVTA_GEO_RESTRICTED: 4009; // @public export const SVTA_HOST_RESOLUTION_ERROR: 3003; // @public export const SVTA_HTTP_TIMEOUT: 3002; // @public export const SVTA_INITIALIZATION_SEGMENT_ERROR: 2031; // @public export const SVTA_INSUFFICIENT_BANDWIDTH: 3009; // @public export const SVTA_INSUFFICIENT_DRM_ROBUSTNESS: 4006; // @public export const SVTA_INSUFFICIENT_OUTPUT_PROTECTION: 4007; // @public export const SVTA_INTERACTIVE_CREATIVE_ERROR: 7409; // @public export const SVTA_INVALID_ACCESS_TOKEN: 4012; // @public export const SVTA_INVALID_COMPOSITION_TRACK_SEGMENT_TIME: 1007; // @public export const SVTA_INVALID_HTTP_CONTENT_TYPE: 3011; // @public export const SVTA_INVALID_MANIFEST: 1002; // @public export const SVTA_INVALID_URI: 3005; // @public export const SVTA_KEY_LOAD_ERROR: 4019; // @public export const SVTA_LICENSE_EXPIRED: 4003; // @public export const SVTA_LICENSE_REQUEST_GENERATION_ERROR: 4021; // @public export const SVTA_LICENSE_RESPONSE_REJECTED: 4016; // @public export const SVTA_LICENSE_SERVER_TIMEOUT: 4005; // @public export const SVTA_LINEAR_AD_ERROR: 7400; // @public export const SVTA_LINEARITY_MISMATCH: 7201; // @public export const SVTA_LIVE_TIMESHIFT_OUT_OF_BOUNDS: 2035; // @public export const SVTA_MANIFEST_FEATURE_UNSUPPORTED: 2039; // @public export const SVTA_MANIFEST_PARSE_ERROR: 2005; // @public export const SVTA_MANIFEST_TRACK_PARSE_ERROR: 2016; // @public export const SVTA_MAX_RETRIES_EXCEEDED: 3008; // @public export const SVTA_MEDIA_CONTENT_UNKNOWN: 1000; // @public export const SVTA_MEDIA_FILE_DISPLAY_ERROR: 7405; // @public export const SVTA_MEDIA_FILE_NOT_FOUND: 7401; // @public export const SVTA_MEDIA_FILE_UNAVAILABLE: 7402; // @public export const SVTA_MEDIA_UNAVAILABLE: 1001; // @public export const SVTA_MEZZANINE_DOWNLOADING: 7407; // @public export const SVTA_MEZZANINE_MISSING: 7406; // @public export const SVTA_MISALIGNED_TRACK_DURATION: 1009; // @public export const SVTA_NETWORK_UNKNOWN: 3000; // @public export const SVTA_NO_MATCHING_CODEC: 2013; // @public export const SVTA_NO_NETWORK_CONNECTION: 3001; // @public export const SVTA_NO_SUPPORTED_AUDIO_TRACK: 2012; // @public export const SVTA_NO_SUPPORTED_MEDIA_TYPE: 7403; // @public export const SVTA_NO_SUPPORTED_VIDEO_TRACK: 2011; // @public export const SVTA_NONLINEAR_AD_ERROR: 7500; // @public export const SVTA_NONLINEAR_AD_FETCH_ERROR: 7502; // @public export const SVTA_NONLINEAR_DIMENSION_MISMATCH: 7501; // @public export const SVTA_OUT_OF_MEMORY: 2028; // @public export const SVTA_PERMISSION_REJECTED: 4018; // @public export const SVTA_PERSISTENT_SESSION_ERROR: 4017; // @public export const SVTA_PICTURE_IN_PICTURE_ERROR: 2038; // @public export const SVTA_PLAYBACK_UNKNOWN: 2000; // @public export const SVTA_PLAYHEAD_EXCEEDS_CONTENT_DURATION: 2010; // @public export const SVTA_PRIMARY_MANIFEST_LOAD_ERROR: 2014; // @public export const SVTA_PRIMARY_MANIFEST_LOAD_TIMEOUT: 2015; // @public export const SVTA_RECEIVER_ALREADY_IN_REMOTE_PLAY: 6008; // @public export const SVTA_RECEIVER_CONNECTION_LOST: 6005; // @public export const SVTA_RECEIVER_CONNECTION_REFUSED: 6004; // @public export const SVTA_RECEIVER_PLAYBACK_ERROR: 6009; // @public export const SVTA_RECEIVER_UNAVAILABLE: 6003; // @public export const SVTA_RECEIVER_UNSUPPORTED_STREAM: 6007; // @public export const SVTA_REJECTED_AD: 7408; // @public export const SVTA_REMOTE_PLAY_UNKNOWN: 6000; // @public export const SVTA_REMUX_ERROR: 2021; // @public export const SVTA_REQUIRED_COMPANION_AD_ERROR: 7602; // @public export const SVTA_RESOURCE_DENIED: 3010; // @public export const SVTA_RESOURCE_NOT_FOUND: 3004; // @public export const SVTA_SECONDARY_MANIFEST_LOAD_ERROR: 2017; // @public export const SVTA_SECONDARY_MANIFEST_PARSE_ERROR: 2018; // @public export const SVTA_SEGMENT_DECRYPTION_ERROR: 4020; // @public export const SVTA_SEGMENT_EXCEEDS_TRACK_BITRATE: 1008; // @public export const SVTA_SEGMENT_LOAD_ERROR: 2029; // @public export const SVTA_SEGMENT_PARSE_ERROR: 2006; // @public export const SVTA_SEGMENT_TIMEOUT_ERROR: 2030; // @public export const SVTA_SENDER_CONNECTION_ERROR: 6002; // @public export const SVTA_SENDER_CONNECTION_LOST: 6006; // @public export const SVTA_SENDER_INITIALIZATION_ERROR: 6001; // @public export const SVTA_SIZE_MISMATCH: 7203; // @public export const SVTA_SWITCHED_TO_HIGHER_LATENCY: 2036; // @public export const SVTA_TIME_SYNC_ERROR: 2032; // @public export const SVTA_TIMED_TEXT_EXCEEDS_CONTENT_DURATION: 5003; // @public export const SVTA_TIMED_TEXT_PARSE_ERROR: 5001; // @public export const SVTA_TIMED_TEXT_RENDER_ERROR: 5002; // @public export const SVTA_TRACK_LOAD_ERROR: 2020; // @public export const SVTA_TRACK_NOT_AVAILABLE: 1003; // @public export const SVTA_TRACK_SWITCH_ERROR: 2019; // @public export const SVTA_UNDEFINED_VAST_ERROR: 7900; // @public export const SVTA_UNKNOWN: 999; // @public export const SVTA_UNSUPPORTED_AUDIO_FORMAT: 1005; // @public export const SVTA_UNSUPPORTED_DRM_SYSTEM: 4008; // @public export const SVTA_UNSUPPORTED_NONLINEAR_AD: 7503; // @public export const SVTA_UNSUPPORTED_TEXT_FORMAT: 1006; // @public export const SVTA_UNSUPPORTED_URI_SCHEME: 3006; // @public export const SVTA_UNSUPPORTED_VIDEO_FORMAT: 1004; // @public export const SVTA_VAST_PARSE_ERROR: 7100; // @public export const SVTA_VAST_SCHEMA_VALIDATION_ERROR: 7101; // @public export const SVTA_VAST_VERSION_UNSUPPORTED: 7102; // @public export const SVTA_VERIFICATION_ERROR: 7410; // @public export const SVTA_VIDEO_BUFFER_UNDERRUN: 2001; // @public export const SVTA_VIDEO_BUFFERING_TIMEOUT: 2003; // @public export const SVTA_VIDEO_DECODE_ERROR: 2007; // @public export const SVTA_VIDEO_DROPPED_FRAMES_EXCEEDED: 2009; // @public export const SVTA_VPAID_ERROR: 7901; // @public export const SVTA_WRAPPER_ERROR: 7300; // @public export const SVTA_WRAPPER_LIMIT_REACHED: 7302; // @public export const SVTA_WRAPPER_TIMEOUT: 7301; // @public export const SvtaAccessibilityErrorCode: { readonly UNKNOWN: typeof SVTA_ACCESSIBILITY_UNKNOWN; readonly TIMED_TEXT_PARSE_ERROR: typeof SVTA_TIMED_TEXT_PARSE_ERROR; readonly TIMED_TEXT_RENDER_ERROR: typeof SVTA_TIMED_TEXT_RENDER_ERROR; readonly TIMED_TEXT_EXCEEDS_CONTENT_DURATION: typeof SVTA_TIMED_TEXT_EXCEEDS_CONTENT_DURATION; readonly AUDIO_DESCRIPTION_EXCEEDS_CONTENT_DURATION: typeof SVTA_AUDIO_DESCRIPTION_EXCEEDS_CONTENT_DURATION; }; // @public export type SvtaAccessibilityErrorCode = ValueOf; // @public export const SvtaAdvertisingErrorCode: { readonly UNKNOWN: typeof SVTA_ADVERTISING_UNKNOWN; readonly AD_BLOCKER_DETECTED: typeof SVTA_AD_BLOCKER_DETECTED; readonly VAST_PARSE_ERROR: typeof SVTA_VAST_PARSE_ERROR; readonly VAST_SCHEMA_VALIDATION_ERROR: typeof SVTA_VAST_SCHEMA_VALIDATION_ERROR; readonly VAST_VERSION_UNSUPPORTED: typeof SVTA_VAST_VERSION_UNSUPPORTED; readonly AD_TYPE_MISMATCH: typeof SVTA_AD_TYPE_MISMATCH; readonly LINEARITY_MISMATCH: typeof SVTA_LINEARITY_MISMATCH; readonly DURATION_MISMATCH: typeof SVTA_DURATION_MISMATCH; readonly SIZE_MISMATCH: typeof SVTA_SIZE_MISMATCH; readonly WRAPPER_ERROR: typeof SVTA_WRAPPER_ERROR; readonly WRAPPER_TIMEOUT: typeof SVTA_WRAPPER_TIMEOUT; readonly WRAPPER_LIMIT_REACHED: typeof SVTA_WRAPPER_LIMIT_REACHED; readonly EMPTY_WRAPPER_RESPONSE: typeof SVTA_EMPTY_WRAPPER_RESPONSE; readonly LINEAR_AD_ERROR: typeof SVTA_LINEAR_AD_ERROR; readonly MEDIA_FILE_NOT_FOUND: typeof SVTA_MEDIA_FILE_NOT_FOUND; readonly MEDIA_FILE_UNAVAILABLE: typeof SVTA_MEDIA_FILE_UNAVAILABLE; readonly NO_SUPPORTED_MEDIA_TYPE: typeof SVTA_NO_SUPPORTED_MEDIA_TYPE; readonly MEDIA_FILE_DISPLAY_ERROR: typeof SVTA_MEDIA_FILE_DISPLAY_ERROR; readonly MEZZANINE_MISSING: typeof SVTA_MEZZANINE_MISSING; readonly MEZZANINE_DOWNLOADING: typeof SVTA_MEZZANINE_DOWNLOADING; readonly REJECTED_AD: typeof SVTA_REJECTED_AD; readonly INTERACTIVE_CREATIVE_ERROR: typeof SVTA_INTERACTIVE_CREATIVE_ERROR; readonly VERIFICATION_ERROR: typeof SVTA_VERIFICATION_ERROR; readonly NONLINEAR_AD_ERROR: typeof SVTA_NONLINEAR_AD_ERROR; readonly NONLINEAR_DIMENSION_MISMATCH: typeof SVTA_NONLINEAR_DIMENSION_MISMATCH; readonly NONLINEAR_AD_FETCH_ERROR: typeof SVTA_NONLINEAR_AD_FETCH_ERROR; readonly UNSUPPORTED_NONLINEAR_AD: typeof SVTA_UNSUPPORTED_NONLINEAR_AD; readonly COMPANION_AD_ERROR: typeof SVTA_COMPANION_AD_ERROR; readonly COMPANION_DIMENSION_MISMATCH: typeof SVTA_COMPANION_DIMENSION_MISMATCH; readonly REQUIRED_COMPANION_AD_ERROR: typeof SVTA_REQUIRED_COMPANION_AD_ERROR; readonly COMPANION_AD_FETCH_ERROR: typeof SVTA_COMPANION_AD_FETCH_ERROR; readonly UNDEFINED_VAST_ERROR: typeof SVTA_UNDEFINED_VAST_ERROR; readonly VPAID_ERROR: typeof SVTA_VPAID_ERROR; readonly EMPTY_VAST_RESPONSE: typeof SVTA_EMPTY_VAST_RESPONSE; }; // @public export type SvtaAdvertisingErrorCode = ValueOf; // @public export const SvtaContentProtectionErrorCode: { readonly UNKNOWN: typeof SVTA_CONTENT_PROTECTION_UNKNOWN; readonly CONCURRENT_STREAM_LIMIT_EXCEEDED: typeof SVTA_CONCURRENT_STREAM_LIMIT_EXCEEDED; readonly ENTITLEMENT_REFUSED: typeof SVTA_ENTITLEMENT_REFUSED; readonly LICENSE_EXPIRED: typeof SVTA_LICENSE_EXPIRED; readonly BAD_LICENSE_REQUEST: typeof SVTA_BAD_LICENSE_REQUEST; readonly LICENSE_SERVER_TIMEOUT: typeof SVTA_LICENSE_SERVER_TIMEOUT; readonly INSUFFICIENT_DRM_ROBUSTNESS: typeof SVTA_INSUFFICIENT_DRM_ROBUSTNESS; readonly INSUFFICIENT_OUTPUT_PROTECTION: typeof SVTA_INSUFFICIENT_OUTPUT_PROTECTION; readonly UNSUPPORTED_DRM_SYSTEM: typeof SVTA_UNSUPPORTED_DRM_SYSTEM; readonly GEO_RESTRICTED: typeof SVTA_GEO_RESTRICTED; readonly DRM_INITIALIZATION_ERROR: typeof SVTA_DRM_INITIALIZATION_ERROR; readonly CDN_UNAUTHORIZED: typeof SVTA_CDN_UNAUTHORIZED; readonly INVALID_ACCESS_TOKEN: typeof SVTA_INVALID_ACCESS_TOKEN; readonly DRM_CERTIFICATE_ERROR: typeof SVTA_DRM_CERTIFICATE_ERROR; readonly DRM_SESSION_ERROR: typeof SVTA_DRM_SESSION_ERROR; readonly DRM_CONFIGURATION_MISSING: typeof SVTA_DRM_CONFIGURATION_MISSING; readonly LICENSE_RESPONSE_REJECTED: typeof SVTA_LICENSE_RESPONSE_REJECTED; readonly PERSISTENT_SESSION_ERROR: typeof SVTA_PERSISTENT_SESSION_ERROR; readonly PERMISSION_REJECTED: typeof SVTA_PERMISSION_REJECTED; readonly KEY_LOAD_ERROR: typeof SVTA_KEY_LOAD_ERROR; readonly SEGMENT_DECRYPTION_ERROR: typeof SVTA_SEGMENT_DECRYPTION_ERROR; readonly LICENSE_REQUEST_GENERATION_ERROR: typeof SVTA_LICENSE_REQUEST_GENERATION_ERROR; }; // @public export type SvtaContentProtectionErrorCode = ValueOf; // @public export const SvtaCustomErrorCode: { readonly UNKNOWN: typeof SVTA_CUSTOM_UNKNOWN; }; // @public export type SvtaCustomErrorCode = ValueOf; // @public export const SvtaErrorCategory: { readonly UNKNOWN: typeof SVTA_ERROR_CATEGORY_UNKNOWN; readonly MEDIA_CONTENT: typeof SVTA_ERROR_CATEGORY_MEDIA_CONTENT; readonly PLAYBACK: typeof SVTA_ERROR_CATEGORY_PLAYBACK; readonly NETWORK: typeof SVTA_ERROR_CATEGORY_NETWORK; readonly CONTENT_PROTECTION: typeof SVTA_ERROR_CATEGORY_CONTENT_PROTECTION; readonly ACCESSIBILITY: typeof SVTA_ERROR_CATEGORY_ACCESSIBILITY; readonly REMOTE_PLAY: typeof SVTA_ERROR_CATEGORY_REMOTE_PLAY; readonly ADVERTISING: typeof SVTA_ERROR_CATEGORY_ADVERTISING; readonly CUSTOM: typeof SVTA_ERROR_CATEGORY_CUSTOM; }; // @public export type SvtaErrorCategory = ValueOf; // @public export type SvtaErrorCode = SvtaAccessibilityErrorCode | SvtaAdvertisingErrorCode | SvtaContentProtectionErrorCode | SvtaCustomErrorCode | SvtaMediaContentErrorCode | SvtaNetworkErrorCode | SvtaPlaybackErrorCode | SvtaRemotePlayErrorCode | SvtaUnknownErrorCode; // @public export const SvtaMediaContentErrorCode: { readonly UNKNOWN: typeof SVTA_MEDIA_CONTENT_UNKNOWN; readonly MEDIA_UNAVAILABLE: typeof SVTA_MEDIA_UNAVAILABLE; readonly INVALID_MANIFEST: typeof SVTA_INVALID_MANIFEST; readonly TRACK_NOT_AVAILABLE: typeof SVTA_TRACK_NOT_AVAILABLE; readonly UNSUPPORTED_VIDEO_FORMAT: typeof SVTA_UNSUPPORTED_VIDEO_FORMAT; readonly UNSUPPORTED_AUDIO_FORMAT: typeof SVTA_UNSUPPORTED_AUDIO_FORMAT; readonly UNSUPPORTED_TEXT_FORMAT: typeof SVTA_UNSUPPORTED_TEXT_FORMAT; readonly INVALID_COMPOSITION_TRACK_SEGMENT_TIME: typeof SVTA_INVALID_COMPOSITION_TRACK_SEGMENT_TIME; readonly SEGMENT_EXCEEDS_TRACK_BITRATE: typeof SVTA_SEGMENT_EXCEEDS_TRACK_BITRATE; readonly MISALIGNED_TRACK_DURATION: typeof SVTA_MISALIGNED_TRACK_DURATION; }; // @public export type SvtaMediaContentErrorCode = ValueOf; // @public export const SvtaNetworkErrorCode: { readonly UNKNOWN: typeof SVTA_NETWORK_UNKNOWN; readonly NO_NETWORK_CONNECTION: typeof SVTA_NO_NETWORK_CONNECTION; readonly HTTP_TIMEOUT: typeof SVTA_HTTP_TIMEOUT; readonly HOST_RESOLUTION_ERROR: typeof SVTA_HOST_RESOLUTION_ERROR; readonly RESOURCE_NOT_FOUND: typeof SVTA_RESOURCE_NOT_FOUND; readonly INVALID_URI: typeof SVTA_INVALID_URI; readonly UNSUPPORTED_URI_SCHEME: typeof SVTA_UNSUPPORTED_URI_SCHEME; readonly DOWNLOAD_ERROR: typeof SVTA_DOWNLOAD_ERROR; readonly MAX_RETRIES_EXCEEDED: typeof SVTA_MAX_RETRIES_EXCEEDED; readonly INSUFFICIENT_BANDWIDTH: typeof SVTA_INSUFFICIENT_BANDWIDTH; readonly RESOURCE_DENIED: typeof SVTA_RESOURCE_DENIED; readonly INVALID_HTTP_CONTENT_TYPE: typeof SVTA_INVALID_HTTP_CONTENT_TYPE; }; // @public export type SvtaNetworkErrorCode = ValueOf; // @public export const SvtaPlaybackErrorCode: { readonly UNKNOWN: typeof SVTA_PLAYBACK_UNKNOWN; readonly VIDEO_BUFFER_UNDERRUN: typeof SVTA_VIDEO_BUFFER_UNDERRUN; readonly AUDIO_BUFFER_UNDERRUN: typeof SVTA_AUDIO_BUFFER_UNDERRUN; readonly VIDEO_BUFFERING_TIMEOUT: typeof SVTA_VIDEO_BUFFERING_TIMEOUT; readonly AUDIO_BUFFERING_TIMEOUT: typeof SVTA_AUDIO_BUFFERING_TIMEOUT; readonly MANIFEST_PARSE_ERROR: typeof SVTA_MANIFEST_PARSE_ERROR; readonly SEGMENT_PARSE_ERROR: typeof SVTA_SEGMENT_PARSE_ERROR; readonly VIDEO_DECODE_ERROR: typeof SVTA_VIDEO_DECODE_ERROR; readonly AUDIO_DECODE_ERROR: typeof SVTA_AUDIO_DECODE_ERROR; readonly VIDEO_DROPPED_FRAMES_EXCEEDED: typeof SVTA_VIDEO_DROPPED_FRAMES_EXCEEDED; readonly PLAYHEAD_EXCEEDS_CONTENT_DURATION: typeof SVTA_PLAYHEAD_EXCEEDS_CONTENT_DURATION; readonly NO_SUPPORTED_VIDEO_TRACK: typeof SVTA_NO_SUPPORTED_VIDEO_TRACK; readonly NO_SUPPORTED_AUDIO_TRACK: typeof SVTA_NO_SUPPORTED_AUDIO_TRACK; readonly NO_MATCHING_CODEC: typeof SVTA_NO_MATCHING_CODEC; readonly PRIMARY_MANIFEST_LOAD_ERROR: typeof SVTA_PRIMARY_MANIFEST_LOAD_ERROR; readonly PRIMARY_MANIFEST_LOAD_TIMEOUT: typeof SVTA_PRIMARY_MANIFEST_LOAD_TIMEOUT; readonly MANIFEST_TRACK_PARSE_ERROR: typeof SVTA_MANIFEST_TRACK_PARSE_ERROR; readonly SECONDARY_MANIFEST_LOAD_ERROR: typeof SVTA_SECONDARY_MANIFEST_LOAD_ERROR; readonly SECONDARY_MANIFEST_PARSE_ERROR: typeof SVTA_SECONDARY_MANIFEST_PARSE_ERROR; readonly TRACK_SWITCH_ERROR: typeof SVTA_TRACK_SWITCH_ERROR; readonly TRACK_LOAD_ERROR: typeof SVTA_TRACK_LOAD_ERROR; readonly REMUX_ERROR: typeof SVTA_REMUX_ERROR; readonly BUFFER_INITIALIZATION_ERROR: typeof SVTA_BUFFER_INITIALIZATION_ERROR; readonly BUFFER_APPEND_ERROR: typeof SVTA_BUFFER_APPEND_ERROR; readonly BUFFER_REMOVE_ERROR: typeof SVTA_BUFFER_REMOVE_ERROR; readonly BUFFER_FULL_ERROR: typeof SVTA_BUFFER_FULL_ERROR; readonly BUFFER_SEEK_OVER_HOLE: typeof SVTA_BUFFER_SEEK_OVER_HOLE; readonly BUFFER_NUDGE_ON_STALL: typeof SVTA_BUFFER_NUDGE_ON_STALL; readonly OUT_OF_MEMORY: typeof SVTA_OUT_OF_MEMORY; readonly SEGMENT_LOAD_ERROR: typeof SVTA_SEGMENT_LOAD_ERROR; readonly SEGMENT_TIMEOUT_ERROR: typeof SVTA_SEGMENT_TIMEOUT_ERROR; readonly INITIALIZATION_SEGMENT_ERROR: typeof SVTA_INITIALIZATION_SEGMENT_ERROR; readonly TIME_SYNC_ERROR: typeof SVTA_TIME_SYNC_ERROR; readonly FRAGMENT_LOAD_ERROR: typeof SVTA_FRAGMENT_LOAD_ERROR; readonly FRAGMENT_TIMEOUT_ERROR: typeof SVTA_FRAGMENT_TIMEOUT_ERROR; readonly LIVE_TIMESHIFT_OUT_OF_BOUNDS: typeof SVTA_LIVE_TIMESHIFT_OUT_OF_BOUNDS; readonly SWITCHED_TO_HIGHER_LATENCY: typeof SVTA_SWITCHED_TO_HIGHER_LATENCY; readonly FULLSCREEN_ERROR: typeof SVTA_FULLSCREEN_ERROR; readonly PICTURE_IN_PICTURE_ERROR: typeof SVTA_PICTURE_IN_PICTURE_ERROR; readonly MANIFEST_FEATURE_UNSUPPORTED: typeof SVTA_MANIFEST_FEATURE_UNSUPPORTED; readonly CONTENT_STEERING_MANIFEST_LOAD_ERROR: typeof SVTA_CONTENT_STEERING_MANIFEST_LOAD_ERROR; readonly CONTENT_STEERING_MANIFEST_PARSE_ERROR: typeof SVTA_CONTENT_STEERING_MANIFEST_PARSE_ERROR; }; // @public export type SvtaPlaybackErrorCode = ValueOf; // @public export const SvtaRemotePlayErrorCode: { readonly UNKNOWN: typeof SVTA_REMOTE_PLAY_UNKNOWN; readonly SENDER_INITIALIZATION_ERROR: typeof SVTA_SENDER_INITIALIZATION_ERROR; readonly SENDER_CONNECTION_ERROR: typeof SVTA_SENDER_CONNECTION_ERROR; readonly RECEIVER_UNAVAILABLE: typeof SVTA_RECEIVER_UNAVAILABLE; readonly RECEIVER_CONNECTION_REFUSED: typeof SVTA_RECEIVER_CONNECTION_REFUSED; readonly RECEIVER_CONNECTION_LOST: typeof SVTA_RECEIVER_CONNECTION_LOST; readonly SENDER_CONNECTION_LOST: typeof SVTA_SENDER_CONNECTION_LOST; readonly RECEIVER_UNSUPPORTED_STREAM: typeof SVTA_RECEIVER_UNSUPPORTED_STREAM; readonly RECEIVER_ALREADY_IN_REMOTE_PLAY: typeof SVTA_RECEIVER_ALREADY_IN_REMOTE_PLAY; readonly RECEIVER_PLAYBACK_ERROR: typeof SVTA_RECEIVER_PLAYBACK_ERROR; }; // @public export type SvtaRemotePlayErrorCode = ValueOf; // @public export const SvtaUnknownErrorCode: { readonly UNKNOWN: typeof SVTA_UNKNOWN; }; // @public export type SvtaUnknownErrorCode = ValueOf; // @public export function vastErrorToSvtaErrorCode(vastError: number): number; // (No @packageDocumentation comment for this package) ``` # @svta/cml-id3 ID3 tag parsing functionality. ## When to use - Read the ID3 frames in an HLS timed metadata sample or an emsg payload: `getId3Frames`, `Id3Frame` - Read the PRIV timestamp that HLS uses to map ID3 time to media time: `getId3Timestamp` - Check that a buffer holds an ID3 tag: `canParseId3` - Match the ID3 emsg scheme in a DASH event stream: `ID3_SCHEME_ID_URI` Do not write your own ID3 header, size, or frame parser. Open an issue for a frame type that is missing. ## Installation ```bash npm i @svta/cml-id3 ``` ## Usage ```typescript import { getId3Frames } from "@svta/cml-id3"; // id3Data holds the bytes of one ID3 tag, for example from an HLS timed metadata sample. function logId3Frames(id3Data: Uint8Array): void { for (const frame of getId3Frames(id3Data)) { console.log(frame.key, frame.data); } } ``` ## API Report File for "@svta/cml-id3" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @public export function canParseId3(data: Uint8Array, offset: number): boolean; // @public export type DecodedId3Frame = { key: string; data: T; info?: any; }; // @internal export function decodeId3Frame(frame: RawId3Frame): Id3Frame | undefined; // @internal export function decodeId3TextFrame(frame: RawId3Frame): DecodedId3Frame | undefined; // @internal export function getId3Data(data: Uint8Array, offset: number): Uint8Array | undefined; // @public export function getId3Frames(id3Data: Uint8Array): Id3Frame[]; // @public export function getId3Timestamp(data: Uint8Array): number | undefined; // @public export const ID3_SCHEME_ID_URI = "https://aomedia.org/emsg/ID3"; // @public export type Id3Frame = DecodedId3Frame; // @internal export function isId3Footer(data: Uint8Array, offset: number): boolean; // @internal export function isId3Header(data: Uint8Array, offset: number): boolean; // @internal export function isId3TimestampFrame(frame: Id3Frame): frame is DecodedId3Frame; // @internal export type RawId3Frame = { type: string; size: number; data: Uint8Array; }; // @internal export function readId3Size(data: Uint8Array, offset: number): number; // @internal (undocumented) export function toArrayBuffer(view: ArrayBufferLike | TypedArray): ArrayBufferLike; // @internal (undocumented) export function toUint8(data: AllowSharedBufferSource, offset?: number, length?: number): Uint8Array; // @internal (undocumented) export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | Uint8ClampedArray; // @internal export function utf8ArrayToStr(array: Uint8Array, exitOnNull?: boolean): string; // (No @packageDocumentation comment for this package) ``` # @svta/cml-iso-8601 ISO 8601 date/time handling functionality. ## When to use - Convert a DASH or HLS ISO 8601 duration such as PT1M30S to seconds: `decodeIso8601Duration` - Format a number of seconds as an ISO 8601 duration: `encodeIso8601Duration` Do not write your own duration regular expression. Open an issue for a duration form that does not decode. ## Installation ```bash npm i @svta/cml-iso-8601 ``` ## Usage ```typescript import { decodeIso8601Duration, encodeIso8601Duration, } from "@svta/cml-iso-8601"; // Encode duration const encoded = encodeIso8601Duration(3661); // encoded === 'PT1H1M1S' // Decode duration const decoded = decodeIso8601Duration("PT1H1M1S"); // decoded === 3661 ``` ## API Report File for "@svta/cml-iso-8601" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @public export function decodeIso8601Duration(isoDuration: string): number; // @public export function encodeIso8601Duration(duration: number): string; // (No @packageDocumentation comment for this package) ``` # @svta/cml-iso-bmff ISO Base Media File Format (ISO BMFF) parsing. ## When to use - Parse the boxes of an init or media segment, with full content for the box types that you need: `readIsoBoxes`, `findIsoBox`, `filterIsoBoxes`, `traverseIsoBoxes` - Read the track timescale, codec, or default sample duration from moov and moof: `readMdhd`, `readStsd`, `readTfhd`, `readTrun` - Read the base media decode time, the segment index, or event messages: `readTfdt`, `readSidx`, `readEmsg` - Read pssh and encryption boxes: `readPssh`, `readTenc`, `readSchm` - Write boxes to a ReadableStream: `createIsoBoxReadableStream`, `IsoBoxReadableStream` - Write or patch boxes, such as inserting emsg or rewriting tfdt: `writeIsoBoxes`, `writeIsoBox`, `writeEmsg`, `writeTfdt` - Read or write a box type that the package does not name: `IsoBoxReadView`, `IsoBoxWriteView` Do not write your own box parser, box walker, or box writer. Open an issue for a box that is missing. ## Installation ```bash npm i @svta/cml-iso-bmff ``` ## Usage - [Reading boxes](https://cml.svta.org/llms/iso-bmff/docs/reading-boxes.md) - Parse ISO BMFF boxes from a buffer - [Writing boxes](https://cml.svta.org/llms/iso-bmff/docs/writing-boxes.md) - Write boxes to byte arrays or streams - [Utilities](https://cml.svta.org/llms/iso-bmff/docs/utilities.md) - Traverse boxes and type guards --- title: Reading Boxes description: Parse ISO BMFF boxes from a buffer --- # Reading Boxes The `readIsoBoxes` function reads boxes from a buffer and returns an array of parsed boxes. By default, the function reads only the box header. To parse the content of a box, provide a reader function for that box type. > [!NOTE] > The function parses container boxes automatically, so they do not need a reader function. The `CONTAINERS` constant lists all known container types. ## Using default config If tree shaking is not a concern, use `defaultReaderConfig()`. It returns a configuration with every available reader: ```typescript import { defaultReaderConfig, readIsoBoxes } from "@svta/cml-iso-bmff"; const buffer = await fetch("https://example.com/bbb.mp4").then((r) => r.arrayBuffer(), ); const boxes = readIsoBoxes(buffer, defaultReaderConfig()); ``` ## Using specific readers For optimal bundle size with tree shaking, import only the readers you need: ```typescript import { readFtyp, readIsoBoxes } from "@svta/cml-iso-bmff"; const buffer = await fetch("https://example.com/bbb.mp4").then((r) => r.arrayBuffer(), ); const boxes = readIsoBoxes(buffer, { readers: { ftyp: readFtyp, }, }); ``` --- title: Utilities description: Traversing boxes and type guards --- # Utilities ## Traversing boxes The `traverseIsoBoxes` function traverses box iterators and returns a generator of boxes. It takes an iterable of boxes and two optional arguments: depth-first traversal and maximum depth. By default, the function traverses depth-first with a maximum depth of infinity. A maximum depth of 0 traverses only the root boxes. ```typescript import { readIsoBoxes, traverseIsoBoxes } from "@svta/cml-iso-bmff"; const boxes = readIsoBoxes(buffer); for (const box of traverseIsoBoxes(boxes)) { console.log(box.type); } ``` ## Type guards The library provides type guards that check whether a box is a container box or a full box. These guards narrow the type of a box in TypeScript. `isContainer` checks whether a box is a container box. ```typescript import { isContainer, type IsoBox } from "@svta/cml-iso-bmff"; const box: IsoBox = { type: "meta", version: 0, flags: 0, boxes: [] }; const container = isContainer(box); ``` `isFullBox` checks whether a box is a full box. ```typescript import { isFullBox } from "@svta/cml-iso-bmff"; const box = { type: "meta", version: 1, flags: 0 }; const fullBox = isFullBox(box); ``` --- title: Writing Boxes description: Write boxes to byte arrays or streams --- # Writing Boxes The `writeIsoBoxes` function writes boxes. It takes an array of streamable items and returns an array of `Uint8Array`s. Streamable items include: - Raw box objects, such as `{ type: 'ftyp', majorBrand: 'isom', minorVersion: 1, compatibleBrands: ['isom'] }` - ArrayBufferViews such as `Uint8Array` and `DataView`, and the box structures that `readIsoBoxes` returns. Those structures implement the `ArrayBufferView` interface. If you provide raw box objects, also provide a configuration object that names the writers for those box types. > [!NOTE] > The function writes container boxes automatically, so they do not need a writer function. The `CONTAINERS` constant lists all known container types. ## Using default config If tree shaking is not a concern, use `defaultWriterConfig()`. It returns a configuration with every available writer: ```typescript import { defaultWriterConfig, writeIsoBoxes, type IsoBoxStreamable } from "@svta/cml-iso-bmff"; const boxes: IsoBoxStreamable[] = [ { type: "ftyp", majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }, { type: "mdat", data: new Uint8Array([ 102, 105, 114, 115, 116, 115, 101, 99, 111, 110, 100, 116, 104, 105, 114, 100, ]), }, ]; const byteArrays = writeIsoBoxes(boxes, defaultWriterConfig()); for (const byteArray of byteArrays) { sourceBuffer.appendBuffer(byteArray); // wait for updateend event } ``` ## Using specific writers For optimal bundle size with tree shaking, import only the writers you need: ```typescript import { writeFtyp, writeIsoBoxes, writeMdat, type IsoBoxStreamable } from "@svta/cml-iso-bmff"; const boxes: IsoBoxStreamable[] = [ { type: "ftyp", majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }, new Uint8Array([ 0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x01, 0x69, 0x73, 0x6f, 0x6d, ]), { type: "mdat", data: new Uint8Array([ 102, 105, 114, 115, 116, 115, 101, 99, 111, 110, 100, 116, 104, 105, 114, 100, ]), }, ]; const byteArrays = writeIsoBoxes(boxes, { writers: { ftyp: writeFtyp, mdat: writeMdat, }, }); for (const byteArray of byteArrays) { sourceBuffer.appendBuffer(byteArray); // wait for updateend event } ``` ## Streaming boxes The `createIsoBoxReadableStream` function writes boxes to a readable stream. It supports both the default config and specific writers: ```typescript import { createIsoBoxReadableStream, defaultWriterConfig, type IsoBoxStreamable, } from "@svta/cml-iso-bmff"; const boxes: IsoBoxStreamable[] = [ { type: "ftyp", majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }, { type: "mdat", data: new Uint8Array([ 102, 105, 114, 115, 116, 115, 101, 99, 111, 110, 100, 116, 104, 105, 114, 100, ]), }, ]; const readableStream = createIsoBoxReadableStream(boxes, defaultWriterConfig()); const stream = await transport.createBidirectionalStream(); readableStream.pipeTo(stream.writable); ``` ## Type safety In TypeScript, cast raw box objects to a box type for type safety. There are two ways. The first way uses a box type from the library: ```typescript import type { FileTypeBox } from "@svta/cml-iso-bmff"; const box: FileTypeBox = { type: "ftyp", majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }; ``` The second way uses the `as const` operator on the `type` property: ```typescript const box = { type: "ftyp" as const, majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }; ``` You can also get a box type from `IsoBoxMap` with the box's FourCC: ```typescript import type { IsoBoxMap } from "@svta/cml-iso-bmff"; const box: IsoBoxMap["ftyp"] = { type: "ftyp", majorBrand: "isom", minorVersion: 1, compatibleBrands: ["isom"], }; ``` ## API Report File for "@svta/cml-iso-bmff" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ValueOf } from '@svta/cml-utils'; // @public export type AdditionalMetadataContainerBox = { type: "meco"; boxes: AdditionalMetadataContainerBoxChild[]; }; // @public export type AdditionalMetadataContainerBoxChild = any; // @public export type AudioRenderingIndicationBox = FullBox & { type: "ardi"; audioRenderingIndication: number; }; // @public export type AudioSampleEntryBox = SampleEntryBox & { type: T; reserved2: number[]; channelcount: number; samplesize: number; preDefined: number; reserved3: number; samplerate: number; boxes: AudioSampleEntryBoxChild[]; }; // @public export type AudioSampleEntryBoxChild = any; // @public export type AudioSampleEntryType = "mp4a" | "enca"; // @public export type Box = { type: T; }; // @public export type ChunkLargeOffsetBox = FullBox & { type: "co64"; entryCount: number; chunkOffset: number[]; }; // @public export type ChunkOffsetBox = FullBox & { type: "stco"; entryCount: number; chunkOffset: number[]; }; // @public (undocumented) export type co64 = ChunkLargeOffsetBox; // @public export type CompactSampleSizeBox = FullBox & { type: "stz2"; fieldSize: number; sampleCount: number; entrySize: number[]; }; // @public export type CompositionTimeToSampleBox = FullBox & { type: "ctts"; entryCount: number; entries: CompositionTimeToSampleEntry[]; }; // @public export type CompositionTimeToSampleEntry = { sampleCount: number; sampleOffset: number; }; // @public export const CONTAINERS: string[]; // @public export function createAudioSampleEntryReader(type: T): (view: IsoBoxReadView) => AudioSampleEntryBox; // @public export function createIsoBoxReadableStream(boxes: Iterable, config: IsoBoxWriteViewConfig): ReadableStream; // @public export function createVisualSampleEntryReader(type: T): (view: IsoBoxReadView) => VisualSampleEntryBox; // @public export const DATA: "data"; // @public export type DataEntryUrlBox = FullBox & { type: "url "; location: string; }; // @public export type DataEntryUrnBox = FullBox & { type: "urn "; name: string; location: string; }; // @public export type DataInformationBox = { type: "dinf"; boxes: DataInformationBoxChild[]; }; // @public export type DataInformationBoxChild = DataReferenceBox; // @public export type DataReferenceBox = FullBox & { type: "dref"; entryCount: number; entries: (DataEntryUrlBox | DataEntryUrnBox)[]; }; // @public export type DecodingTimeSample = { sampleCount: number; sampleDelta: number; }; // @public export type DecodingTimeToSampleBox = FullBox & { type: "stts"; entryCount: number; entries: DecodingTimeSample[]; }; // @public export function defaultReaderConfig(): IsoBoxReadViewConfig; // @public export function defaultWriterConfig(): IsoBoxWriteViewConfig; // @public export type DegradationPriorityBox = FullBox & { type: "stdp"; priority: number[]; }; // @public export type EditBox = { type: "edts"; boxes: EditBoxChild[]; }; // @public export type EditBoxChild = EditListBox; // @public export type EditListBox = FullBox & { type: "elst"; entryCount: number; entries: EditListEntry[]; }; // @public export type EditListEntry = { segmentDuration: number; mediaTime: number; mediaRateInteger: number; mediaRateFraction: number; }; // @public export type EncryptedSample = { initializationVector?: Uint8Array; subsampleEncryption?: SubsampleEncryption[]; }; // @public export type Entity = { entityId: number; }; // @public export type EventMessageBox = EventMessageBoxV0 | EventMessageBoxV1; // @public export type EventMessageBoxBase = FullBox & { type: "emsg"; schemeIdUri: string; value: string; timescale: number; eventDuration: number; id: number; messageData: Uint8Array; }; // @public export type EventMessageBoxV0 = EventMessageBoxBase & { version: 0; presentationTimeDelta: number; }; // @public export type EventMessageBoxV1 = EventMessageBoxBase & { version: 1; presentationTime: number; }; // @public export type ExtendedLanguageBox = FullBox & { type: "elng"; extendedLanguage: string; }; // @public export type FileTypeBox = TypeBox<"ftyp">; // @public export function filterIsoBoxes(boxes: Iterable, callback: (box: T) => box is S, config?: TraverseIsoBoxesConfig): S[]; // @public (undocumented) export function filterIsoBoxes(boxes: Iterable, callback: (box: T) => boolean, config?: TraverseIsoBoxesConfig): T[]; // @public export function findIsoBox(boxes: Iterable, callback: (box: T) => box is S, config?: TraverseIsoBoxesConfig): S | null; // @public (undocumented) export function findIsoBox(boxes: Iterable, callback: (box: T) => boolean, config?: TraverseIsoBoxesConfig): T | null; // @public export function fourCcToUint32(str: string): number; // @public export type FreeSpaceBox = { type: T; data: Uint8Array; }; // @public export type FullBox = { version: number; flags: number; }; // @public export type GroupsListBox = { type: "grpl"; boxes: GroupsListBoxChild[]; }; // @public export type GroupsListBoxChild = any; // @public export type HandlerReferenceBox = FullBox & { type: "hdlr"; preDefined: number; handlerType: string; reserved: number[]; name: string; }; // @public export type HintMediaHeaderBox = FullBox & { type: "hmhd"; maxPDUsize: number; avgPDUsize: number; maxbitrate: number; avgbitrate: number; }; // @public export type IdentifiedMediaDataBox = { type: "imda"; imdaIdentifier: number; data: Uint8Array; }; // @public export const INT: "int"; // @public export type IpmpInfoBox = FullBox & { type: "imif"; ipmpDescr: any[]; }; // @public export function isContainer>(box: T): box is T & IsoBoxContainer; // @public export function isFullBox(box: any): box is FullBox; // @public export function isIsoBoxType(type: T, box: unknown): box is IsoBoxMap[T]; // @public export type IsoBox = IsoBoxMap[keyof IsoBoxMap]; // @public export type IsoBoxContainer = IsoBoxContainerMap[keyof IsoBoxContainerMap]; // @public export type IsoBoxContainerMap = { dinf: DataInformationBox; edts: EditBox; grpl: GroupsListBox; mdia: MediaBox; meco: AdditionalMetadataContainerBox; mfra: MovieFragmentRandomAccessBox; minf: MediaInformationBox; moof: MovieFragmentBox; moov: MovieBox; mvex: MovieExtendsBox; schi: SchemeInformationBox; sinf: ProtectionSchemeInformationBox; stbl: SampleTableBox; strk: SubTrackBox; traf: TrackFragmentBox; trak: TrackBox; tref: TrackReferenceBox; udta: UserDataBox; vttc: WebVttCueBox; }; // @public export type IsoBoxData = ArrayBufferLike | ArrayBufferView; // @public export const IsoBoxFields: { DATA: typeof DATA; INT: typeof INT; STRING: typeof STRING; TEMPLATE: typeof TEMPLATE; UINT: typeof UINT; UTF8: typeof UTF8; }; // @public (undocumented) export type IsoBoxFields = ValueOf; // @public export type IsoBoxMap = { ardi: AudioRenderingIndicationBox; avc1: VisualSampleEntryBox<"avc1">; avc2: VisualSampleEntryBox<"avc2">; avc3: VisualSampleEntryBox<"avc3">; avc4: VisualSampleEntryBox<"avc4">; co64: ChunkLargeOffsetBox; ctts: CompositionTimeToSampleBox; dinf: DataInformationBox; dref: DataReferenceBox; edts: EditBox; elng: ExtendedLanguageBox; elst: EditListBox; emsg: EventMessageBox; enca: AudioSampleEntryBox<"enca">; encv: VisualSampleEntryBox<"encv">; free: FreeSpaceBox<"free">; frma: OriginalFormatBox; ftyp: FileTypeBox; grpl: GroupsListBox; hdlr: HandlerReferenceBox; hev1: VisualSampleEntryBox<"hev1">; hmhd: HintMediaHeaderBox; hvc1: VisualSampleEntryBox<"hvc1">; iden: WebVttCueIdBox; iinf: ItemInfoBox; iloc: ItemLocationBox; imda: IdentifiedMediaDataBox; imif: IpmpInfoBox; infe: ItemInfoEntry; ipro: ItemProtectionBox; iref: ItemReferenceBox; kind: TrackKindBox; labl: LabelBox; mdat: MediaDataBox; mdhd: MediaHeaderBox; mdia: MediaBox; meco: AdditionalMetadataContainerBox; mehd: MovieExtendsHeaderBox; meta: MetaBox; mfhd: MovieFragmentHeaderBox; mfra: MovieFragmentRandomAccessBox; mfro: MovieFragmentRandomAccessOffsetBox; minf: MediaInformationBox; moof: MovieFragmentBox; moov: MovieBox; mp4a: AudioSampleEntryBox<"mp4a">; mvex: MovieExtendsBox; mvhd: MovieHeaderBox; nmhd: NullMediaHeaderBox; payl: WebVttCuePayloadBox; pitm: PrimaryItemBox; prft: ProducerReferenceTimeBox; prsl: PreselectionGroupBox; pssh: ProtectionSystemSpecificHeaderBox; saio: SampleAuxiliaryInformationOffsetsBox; saiz: SampleAuxiliaryInformationSizesBox; sbgp: SampleToGroupBox; schi: SchemeInformationBox; schm: SchemeTypeBox; sdtp: SampleDependencyTypeBox; senc: SampleEncryptionBox; sgpd: SampleGroupDescriptionBox; sidx: SegmentIndexBox; sinf: ProtectionSchemeInformationBox; skip: FreeSpaceBox<"skip">; smhd: SoundMediaHeaderBox; ssix: SubsegmentIndexBox; stbl: SampleTableBox; stco: ChunkOffsetBox; stdp: DegradationPriorityBox; sthd: SubtitleMediaHeaderBox; strk: SubTrackBox; stsc: SampleToChunkBox; stsd: SampleDescriptionBox; stsh: ShadowSyncSampleBox; stss: SyncSampleBox; stsz: SampleSizeBox; sttg: WebVttSettingsBox; stts: DecodingTimeToSampleBox; styp: SegmentTypeBox; subs: SubsampleInformationBox; stz2: CompactSampleSizeBox; tenc: TrackEncryptionBox; tfdt: TrackFragmentBaseMediaDecodeTimeBox; tfhd: TrackFragmentHeaderBox; tfra: TrackFragmentRandomAccessBox; tkhd: TrackHeaderBox; traf: TrackFragmentBox; trak: TrackBox; tref: TrackReferenceBox; trex: TrackExtendsBox; trun: TrackRunBox; udta: UserDataBox; "url ": DataEntryUrlBox; "urn ": DataEntryUrnBox; vlab: WebVttSourceLabelBox; vmhd: VideoMediaHeaderBox; vttc: WebVttCueBox; vttC: WebVttConfigurationBox; vtte: WebVttEmptySampleBox; }; // Warning: (ae-forgotten-export) The symbol "ReadableStreamBase" needs to be exported by the entry point index.d.ts // // @public export class IsoBoxReadableStream extends ReadableStreamBase { constructor(boxes: Iterable, config: IsoBoxWriteViewConfig); } // @public export type IsoBoxReader = (view: IsoBoxReadView, type: T) => B; // @public export type IsoBoxReaderMap = Record; // @public export type IsoBoxReaderReturn = { [K in keyof T]: T[K] extends ((...args: never[]) => infer R) ? ParsedBox : never; }[keyof T] | ParsedIsoBox; // @public export class IsoBoxReadView { [Symbol.iterator](): Generator; constructor(raw: IsoBoxData, config?: IsoBoxReadViewConfig); get buffer(): ArrayBufferLike; get byteLength(): number; get byteOffset(): number; get bytesRemaining(): number; get cursor(): number; get done(): boolean; jump: (size: number) => void; readArray: (type: T, size: number, length: number) => IsoFieldTypeMap[T][]; readBox: () => ParsedBox; readBoxes: (length?: number) => T[]; readData: (size: number) => Uint8Array; readEntries: (length: number, map: () => T) => T[]; readFullBox: () => FullBox; readInt: (size: number) => number; readString: (size: number) => string; readTemplate: (size: number) => number; readUint: (size: number) => number; readUtf8: (size?: number) => string; slice: (offset: number, size: number) => IsoBoxReadView; } // @public export type IsoBoxReadViewConfig = { readers?: IsoBoxReaderMap; }; // @public export type IsoBoxStreamable = IsoBox | ParsedIsoBox | ParsedBox | ArrayBufferView; // @public export type IsoBoxType = `${string}${string}${string}${string}`; // @public export type IsoBoxWriter = (box: B, config: IsoBoxWriteViewConfig) => ArrayBufferView; // @public export type IsoBoxWriterMap = Record>; // @public export class IsoBoxWriteView { constructor(type: string, size: number); get buffer(): ArrayBufferLike; get byteLength(): number; get byteOffset(): number; writeArray: (data: number[], type: T, size: number, length: number) => void; writeBoxHeader: (type: string, size: number) => void; writeBytes: (data: Uint8Array | Uint8Array[]) => void; writeFullBox(version: number, flags: number): void; writeInt: (value: number, size: number) => void; writeString: (value: string) => void; writeTemplate: (value: number, size: number) => void; writeTerminatedString: (value: string) => void; writeUint: (value: number, size: number) => void; writeUtf8TerminatedString: (value: string) => void; } // @public export type IsoBoxWriteViewConfig = { writers: IsoBoxWriterMap; }; // @public export type IsoFieldTypeMap = { [IsoBoxFields.UINT]: number; [IsoBoxFields.INT]: number; [IsoBoxFields.TEMPLATE]: number; [IsoBoxFields.STRING]: string; [IsoBoxFields.DATA]: Uint8Array; [IsoBoxFields.UTF8]: string; }; // @public export type IsoTypedParsedBox = ParsedBox; // @public export type ItemExtent = { extentIndex?: number; extentOffset: number; extentLength: number; }; // @public export type ItemInfoBox = { type: "iinf"; boxes: ItemInfoBoxChild[]; entryCount: number; }; // @public export type ItemInfoBoxChild = ItemInfoEntry; // @public export type ItemInfoEntry = FullBox & { type: "infe"; itemId: number; itemProtectionIndex: number; itemName: string; contentType: string; contentEncoding?: string; extensionType?: string; }; // @public export type ItemLocation = { itemId: number; constructionMethod?: number; dataReferenceIndex: number; baseOffset: number; extents: ItemExtent[]; }; // @public export type ItemLocationBox = FullBox & { type: "iloc"; offsetSize: number; lengthSize: number; baseOffsetSize: number; indexSize?: number; itemCount: number; items: ItemLocation[]; }; // @public export type ItemProtectionBox = { type: "ipro"; boxes: ItemProtectionBoxChild[]; protectionCount: number; }; // @public export type ItemProtectionBoxChild = ProtectionSchemeInformationBox; // @public export type ItemReferenceBox = { type: "iref"; boxes: ItemReferenceBoxChild[]; }; // @public export type ItemReferenceBoxChild = SingleItemTypeReferenceBox; // @public export type LabelBox = FullBox & { type: "labl"; isGroupLabel: boolean; labelId: number; language: string; label: string; }; // @public export type MediaBox = { type: "mdia"; boxes: MediaBoxChild[]; }; // @public export type MediaBoxChild = MediaHeaderBox | HandlerReferenceBox | MediaInformationBox; // @public export type MediaDataBox = { type: "mdat"; data: Uint8Array; }; // @public export type MediaHeaderBox = FullBox & { type: "mdhd"; creationTime: number; modificationTime: number; timescale: number; duration: number; language: string; preDefined: number; }; // @public export type MediaInformationBox = { type: "minf"; boxes: MediaInformationBoxChild[]; }; // @public export type MediaInformationBoxChild = VideoMediaHeaderBox | SoundMediaHeaderBox | HintMediaHeaderBox | NullMediaHeaderBox | DataInformationBox | SampleTableBox | SubtitleMediaHeaderBox; // @public export type MetaBox = FullBox & { type: "meta"; boxes: MetaBoxChild[]; }; // @public export type MetaBoxChild = HandlerReferenceBox | PrimaryItemBox | DataInformationBox | ItemLocationBox | ItemProtectionBox | ItemInfoBox | ItemReferenceBox | GroupsListBox; // @public export type MovieBox = { type: "moov"; boxes: MovieBoxChild[]; }; // @public export type MovieBoxChild = MovieHeaderBox | TrackBox | MovieExtendsBox | UserDataBox; // @public export type MovieExtendsBox = { type: "mvex"; boxes: MovieExtendsBoxChild[]; }; // @public export type MovieExtendsBoxChild = MovieExtendsHeaderBox | TrackExtendsBox; // @public export type MovieExtendsHeaderBox = FullBox & { type: "mehd"; fragmentDuration: number; }; // @public export type MovieFragmentBox = { type: "moof"; boxes: MovieFragmentBoxChild[]; }; // @public export type MovieFragmentBoxChild = MovieFragmentHeaderBox | TrackFragmentBox; // @public export type MovieFragmentHeaderBox = FullBox & { type: "mfhd"; sequenceNumber: number; }; // @public export type MovieFragmentRandomAccessBox = { type: "mfra"; boxes: MovieFragmentRandomAccessBoxChild[]; }; // @public export type MovieFragmentRandomAccessBoxChild = TrackFragmentRandomAccessBox | MovieFragmentRandomAccessOffsetBox; // @public export type MovieFragmentRandomAccessOffsetBox = FullBox & { type: "mfro"; mfraSize: number; }; // @public export type MovieHeaderBox = FullBox & { type: "mvhd"; creationTime: number; modificationTime: number; timescale: number; duration: number; rate: number; volume: number; reserved1: number; reserved2: number[]; matrix: number[]; preDefined: number[]; nextTrackId: number; }; // @public export type NullMediaHeaderBox = FullBox & { type: "nmhd"; }; // @public export type OriginalFormatBox = { type: "frma"; dataFormat: number; }; // @public export type ParsedBox = (T extends IsoBox ? T & Omit : T) & { view: IsoBoxReadView; size: number; largesize?: number; usertype?: number[]; }; // @public export type ParsedIsoBox = ParsedBox; // @public export type PreselectionGroupBox = FullBox & { type: "prsl"; groupId: number; numEntitiesInGroup: number; entities: Entity[]; preselectionTag?: string; selectionPriority?: number; interleavingTag?: string; boxes: PreselectionGroupBoxChild[]; }; // @public export type PreselectionGroupBoxChild = any; // @public export type PrimaryItemBox = FullBox & { type: "pitm"; itemId: number; }; // @public export type ProducerReferenceTimeBox = FullBox & { type: "prft"; referenceTrackId: number; ntpTimestampSec: number; ntpTimestampFrac: number; mediaTime: number; }; // @public export type ProtectionSchemeInformationBox = { type: "sinf"; boxes: ProtectionSchemeInformationBoxChild[]; }; // @public export type ProtectionSchemeInformationBoxChild = OriginalFormatBox | IpmpInfoBox | SchemeTypeBox | SchemeInformationBox; // @public export type ProtectionSystemSpecificHeaderBox = FullBox & { type: "pssh"; systemId: number[]; kidCount: number; kid: number[]; dataSize: number; data: number[]; }; // @public export function readArdi(view: IsoBoxReadView): AudioRenderingIndicationBox; // @public export function readAudioSampleEntryBox(type: T, view: IsoBoxReadView): AudioSampleEntryBox; // @public export function readAvc1(view: IsoBoxReadView): VisualSampleEntryBox<"avc1">; // @public export function readAvc2(view: IsoBoxReadView): VisualSampleEntryBox<"avc2">; // @public export function readAvc3(view: IsoBoxReadView): VisualSampleEntryBox<"avc3">; // @public export function readAvc4(view: IsoBoxReadView): VisualSampleEntryBox<"avc4">; // @public export function readCtts(view: IsoBoxReadView): CompositionTimeToSampleBox; // @public export function readDref(view: IsoBoxReadView): DataReferenceBox; // @public export function readElng(view: IsoBoxReadView): ExtendedLanguageBox; // @public export function readElst(view: IsoBoxReadView): EditListBox; // @public export function readEmsg(view: IsoBoxReadView): EventMessageBox; // @public export function readEnca(view: IsoBoxReadView): AudioSampleEntryBox<"enca">; // @public export function readEncv(view: IsoBoxReadView): VisualSampleEntryBox<"encv">; // @public export function readFree(view: IsoBoxReadView): FreeSpaceBox; // @public export function readFrma(view: IsoBoxReadView): OriginalFormatBox; // @public export function readFtyp(view: IsoBoxReadView): FileTypeBox; // @public export function readHdlr(view: IsoBoxReadView): HandlerReferenceBox; // @public export function readHev1(view: IsoBoxReadView): VisualSampleEntryBox<"hev1">; // @public export function readHvc1(view: IsoBoxReadView): VisualSampleEntryBox<"hvc1">; // @public export function readIden(view: IsoBoxReadView): WebVttCueIdBox; // @public export function readImda(view: IsoBoxReadView): IdentifiedMediaDataBox; // @public export function readIsoBoxes(raw: IsoBoxData, config: IsoBoxReadViewConfig & { readers: R; }): IsoBoxReaderReturn[]; // @public (undocumented) export function readIsoBoxes(raw: IsoBoxData, config?: IsoBoxReadViewConfig): ParsedIsoBox[]; // @public export function readKind(view: IsoBoxReadView): TrackKindBox; // @public export function readLabl(view: IsoBoxReadView): LabelBox; // @public export function readMdat(view: IsoBoxReadView): MediaDataBox; // @public export function readMdhd(view: IsoBoxReadView): MediaHeaderBox; // @public export function readMehd(view: IsoBoxReadView): MovieExtendsHeaderBox; // @public export function readMeta(view: IsoBoxReadView): MetaBox; // @public export function readMfhd(view: IsoBoxReadView): MovieFragmentHeaderBox; // @public export function readMfro(view: IsoBoxReadView): MovieFragmentRandomAccessOffsetBox; // @public export function readMp4a(view: IsoBoxReadView): AudioSampleEntryBox<"mp4a">; // @public export function readMvhd(view: IsoBoxReadView): MovieHeaderBox; // @public export function readPayl(view: IsoBoxReadView): WebVttCuePayloadBox; // @public export function readPrft(view: IsoBoxReadView): ProducerReferenceTimeBox; // @public export function readPrsl(view: IsoBoxReadView): PreselectionGroupBox; // @public export function readPssh(view: IsoBoxReadView): ProtectionSystemSpecificHeaderBox; // @public export function readSchm(view: IsoBoxReadView): SchemeTypeBox; // @public export function readSdtp(view: IsoBoxReadView): SampleDependencyTypeBox; // @public export function readSidx(view: IsoBoxReadView): SegmentIndexBox; // @public export function readSkip(view: IsoBoxReadView): FreeSpaceBox<"skip">; // @public export function readSmhd(view: IsoBoxReadView): SoundMediaHeaderBox; // @public export function readSsix(view: IsoBoxReadView): SubsegmentIndexBox; // @public export function readSthd(view: IsoBoxReadView): SubtitleMediaHeaderBox; // @public export function readStsd(view: IsoBoxReadView): SampleDescriptionBox; // @public export function readStss(view: IsoBoxReadView): SyncSampleBox; // @public export function readSttg(view: IsoBoxReadView): WebVttSettingsBox; // @public export function readStts(view: IsoBoxReadView): DecodingTimeToSampleBox; // @public export function readStyp(view: IsoBoxReadView): SegmentTypeBox; // @public export function readSubs(view: IsoBoxReadView): SubsampleInformationBox; // @public export function readTenc(view: IsoBoxReadView): TrackEncryptionBox; // @public export function readTfdt(view: IsoBoxReadView): TrackFragmentBaseMediaDecodeTimeBox; // @public export function readTfhd(view: IsoBoxReadView): TrackFragmentHeaderBox; // @public export function readTfra(view: IsoBoxReadView): TrackFragmentRandomAccessBox; // @public export function readTkhd(view: IsoBoxReadView): TrackHeaderBox; // @public export function readTrex(view: IsoBoxReadView): TrackExtendsBox; // @public export function readTrun(view: IsoBoxReadView): TrackRunBox; // @public export function readUrl(view: IsoBoxReadView): DataEntryUrlBox; // @public export function readUrn(view: IsoBoxReadView): DataEntryUrnBox; // @public export function readVisualSampleEntryBox(type: T, view: IsoBoxReadView): VisualSampleEntryBox; // @public export function readVlab(view: IsoBoxReadView): WebVttSourceLabelBox; // @public export function readVmhd(view: IsoBoxReadView): VideoMediaHeaderBox; // @public export function readVttC(view: IsoBoxReadView): WebVttConfigurationBox; // @public export function readVtte(_: IsoBoxReadView): WebVttEmptySampleBox; // @public export type SampleAuxiliaryInformationOffsetsBox = FullBox & { type: "saio"; auxInfoType?: number; auxInfoTypeParameter?: number; entryCount: number; offset: number[]; }; // @public export type SampleAuxiliaryInformationSizesBox = FullBox & { type: "saiz"; auxInfoType?: number; auxInfoTypeParameter?: number; defaultSampleInfoSize: number; sampleCount: number; sampleInfoSize?: number[]; }; // @public export type SampleDependencyTypeBox = FullBox & { type: "sdtp"; sampleDependencyTable: number[]; }; // @public export type SampleDescriptionBox = FullBox & { type: "stsd"; entryCount: number; entries: E[]; }; // @public export type SampleEncryptionBox = FullBox & { type: "senc"; sampleCount: number; samples: EncryptedSample[]; }; // @public export type SampleEntryBox = { type: T; reserved1: number[]; dataReferenceIndex: number; boxes: C[]; }; // @public export type SampleGroupDescriptionBox = FullBox & { type: "sgpd"; groupingType: number; defaultLength?: number; entryCount: number; entries: any[]; }; // @public export type SampleSizeBox = FullBox & { type: "stsz"; sampleSize: number; sampleCount: number; entrySize?: number[]; }; // @public export type SampleTableBox = { type: "stbl"; boxes: SampleTableBoxChild[]; }; // @public export type SampleTableBoxChild = SampleDescriptionBox | DecodingTimeToSampleBox | CompositionTimeToSampleBox | SampleToChunkBox | SampleSizeBox | ChunkOffsetBox | SyncSampleBox | ShadowSyncSampleBox | DegradationPriorityBox | SampleDependencyTypeBox | SampleToGroupBox | SampleGroupDescriptionBox; // @public export type SampleToChunkBox = FullBox & { type: "stsc"; entryCount: number; entries: SampleToChunkEntry[]; }; // @public export type SampleToChunkEntry = { firstChunk: number; samplesPerChunk: number; sampleDescriptionIndex: number; }; // @public export type SampleToGroupBox = FullBox & { type: "sbgp"; groupingType: number; groupingTypeParameter?: number; entryCount: number; entries: SampleToGroupEntry[]; }; // @public export type SampleToGroupEntry = { sampleCount: number; groupDescriptionIndex: number; }; // @public export type SchemeInformationBox = { type: "schi"; boxes: SchemeInformationBoxChild[]; }; // @public export type SchemeInformationBoxChild = TrackEncryptionBox; // @public export type SchemeTypeBox = FullBox & { type: "schm"; schemeType: number; schemeVersion: number; schemeUri?: string; }; // @public export type SegmentIndexBox = FullBox & { type: "sidx"; referenceId: number; timescale: number; earliestPresentationTime: number; firstOffset: number; reserved: number; references: SegmentIndexReference[]; }; // @public export type SegmentIndexReference = { reference: number; subsegmentDuration: number; sap: number; referenceType: number; referencedSize: number; startsWithSap: number; sapType: number; sapDeltaTime: number; }; // @public export type SegmentTypeBox = TypeBox<"styp">; // @public export type ShadowSyncEntry = { shadowedSampleNumber: number; syncSampleNumber: number; }; // @public export type ShadowSyncSampleBox = FullBox & { type: "stsh"; entryCount: number; entries: ShadowSyncEntry[]; }; // @public export type SingleItemTypeReferenceBox = { fromItemId: number; referenceCount: number; toItemId: number[]; }; // @public export type SoundMediaHeaderBox = FullBox & { type: "smhd"; balance: number; reserved: number; }; // @public export const STRING: "string"; // @public (undocumented) export type stz2 = CompactSampleSizeBox; // @public export type Subsample = { subsampleSize: number; subsamplePriority: number; discardable: number; codecSpecificParameters: number; }; // @public export type SubsampleEncryption = { bytesOfClearData: number; bytesOfProtectedData: number; }; // @public export type SubsampleEntry = { sampleDelta: number; subsampleCount: number; subsamples: Subsample[]; }; // @public export type SubsampleInformationBox = FullBox & { type: "subs"; entryCount: number; entries: SubsampleEntry[]; }; // @public export type Subsegment = { rangesCount: number; ranges: SubsegmentRange[]; }; // @public export type SubsegmentIndexBox = FullBox & { type: "ssix"; subsegmentCount: number; subsegments: Subsegment[]; }; // @public export type SubsegmentRange = { level: number; rangeSize: number; }; // @public export type SubtitleMediaHeaderBox = FullBox & { type: "sthd"; }; // @public export type SubTrackBox = { type: "strk"; boxes: SubTrackBoxChild[]; }; // @public export type SubTrackBoxChild = any; // @public export type SyncSample = { sampleNumber: number; }; // @public export type SyncSampleBox = FullBox & { type: "stss"; entryCount: number; entries: SyncSample[]; }; // @public export const TEMPLATE: "template"; // @public export type TrackBox = { type: "trak"; boxes: TrackBoxChild[]; }; // @public export type TrackBoxChild = TrackHeaderBox | TrackReferenceBox | EditBox | MediaBox | UserDataBox; // @public export type TrackEncryptionBox = FullBox & { type: "tenc"; defaultIsEncrypted: number; defaultIvSize: number; defaultKid: number[]; }; // @public export type TrackExtendsBox = FullBox & { type: "trex"; trackId: number; defaultSampleDescriptionIndex: number; defaultSampleDuration: number; defaultSampleSize: number; defaultSampleFlags: number; }; // @public export type TrackFragmentBaseMediaDecodeTimeBox = FullBox & { type: "tfdt"; baseMediaDecodeTime: number; }; // @public export type TrackFragmentBox = { type: "traf"; boxes: TrackFragmentBoxChild[]; }; // @public export type TrackFragmentBoxChild = TrackFragmentHeaderBox | TrackFragmentBaseMediaDecodeTimeBox | TrackRunBox | SampleAuxiliaryInformationSizesBox | SampleAuxiliaryInformationOffsetsBox | SampleEncryptionBox | SubsampleInformationBox; // @public export type TrackFragmentHeaderBox = FullBox & { type: "tfhd"; trackId: number; baseDataOffset?: number; sampleDescriptionIndex?: number; defaultSampleDuration?: number; defaultSampleSize?: number; defaultSampleFlags?: number; }; // @public export type TrackFragmentRandomAccessBox = FullBox & { type: "tfra"; trackId: number; reserved: number; numberOfEntry: number; lengthSizeOfTrafNum: number; lengthSizeOfTrunNum: number; lengthSizeOfSampleNum: number; entries: TrackFragmentRandomAccessEntry[]; }; // @public export type TrackFragmentRandomAccessEntry = { time: number; moofOffset: number; trafNumber: number; trunNumber: number; sampleNumber: number; }; // @public export type TrackHeaderBox = FullBox & { type: "tkhd"; creationTime: number; modificationTime: number; trackId: number; reserved1: number; duration: number; reserved2: number[]; layer: number; alternateGroup: number; volume: number; reserved3: number; matrix: number[]; width: number; height: number; }; // @public export type TrackKindBox = FullBox & { type: "kind"; schemeUri: string; value: string; }; // @public export type TrackReferenceBox = { type: "tref"; boxes: TrackReferenceBoxChild[]; }; // @public export type TrackReferenceBoxChild = TrackReferenceTypeBox; // @public export type TrackReferenceTypeBox = { type: "tref"; trackIds: number[]; }; // @public export type TrackRunBox = FullBox & { type: "trun"; sampleCount: number; dataOffset?: number; firstSampleFlags?: number; samples: TrackRunSample[]; }; // @public export type TrackRunSample = { sampleDuration?: number; sampleSize?: number; sampleFlags?: number; sampleCompositionTimeOffset?: number; }; // @public export function traverseIsoBoxes(boxes: Iterable, config?: TraverseIsoBoxesConfig): Generator; // @public export type TraverseIsoBoxesConfig = { depthFirst?: boolean; maxDepth?: number; }; // @public export type TypeBox = { type: T; majorBrand: string; minorVersion: number; compatibleBrands: string[]; }; // @public export const UINT: "uint"; // @public (undocumented) export type url = DataEntryUrlBox; // @public (undocumented) export type urn = DataEntryUrnBox; // @public export type UserDataBox = { type: "udta"; boxes: UserDataBoxChild[]; }; // @public export type UserDataBoxChild = any; // @public export const UTF8: "utf8"; // @public export type VideoMediaHeaderBox = FullBox & { type: "vmhd"; graphicsmode: number; opcolor: number[]; }; // @public export type VisualSampleEntryBox = SampleEntryBox & { type: T; preDefined1: number; reserved2: number; preDefined2: number[]; width: number; height: number; horizresolution: number; vertresolution: number; reserved3: number; frameCount: number; compressorName: number[]; depth: number; preDefined3: number; boxes: VisualSampleEntryBoxChild[]; }; // @public export type VisualSampleEntryBoxChild = any; // @public export type VisualSampleEntryType = "avc1" | "avc2" | "avc3" | "avc4" | "hev1" | "hvc1" | "encv"; // @public export type WebVttConfigurationBox = { type: "vttC"; config: string; }; // @public export type WebVttCueBox = { type: "vttc"; boxes: WebVttCueChild[]; }; // @public export type WebVttCueChild = WebVttCueIdBox | WebVttCuePayloadBox; // @public export type WebVttCueIdBox = { type: "iden"; cueId: string; }; // @public export type WebVttCuePayloadBox = { type: "payl"; cueText: string; }; // @public export type WebVttEmptySampleBox = { type: "vtte"; }; // @public export type WebVttSettingsBox = { type: "sttg"; settings: string; }; // @public export type WebVttSourceLabelBox = { type: "vlab"; sourceLabel: string; }; // @public export function writeArdi(box: AudioRenderingIndicationBox): IsoBoxWriteView; // @public export function writeAudioSampleEntryBox(box: AudioSampleEntryBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeAvc1(box: VisualSampleEntryBox<"avc1">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeAvc2(box: VisualSampleEntryBox<"avc2">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeAvc3(box: VisualSampleEntryBox<"avc3">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeAvc4(box: VisualSampleEntryBox<"avc4">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeCtts(box: CompositionTimeToSampleBox): IsoBoxWriteView; // @public export function writeDref(box: DataReferenceBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeElng(box: ExtendedLanguageBox): IsoBoxWriteView; // @public export function writeElst(box: EditListBox): IsoBoxWriteView; // @public export function writeEmsg(box: EventMessageBox): IsoBoxWriteView; // @public export function writeEnca(box: AudioSampleEntryBox<"enca">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeEncv(box: VisualSampleEntryBox<"encv">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeFree(box: FreeSpaceBox<"free">): IsoBoxWriteView; // @public export function writeFrma(box: OriginalFormatBox): IsoBoxWriteView; // @public export function writeFtyp(box: FileTypeBox): IsoBoxWriteView; // @public export function writeHdlr(box: HandlerReferenceBox): IsoBoxWriteView; // @public export function writeHev1(box: VisualSampleEntryBox<"hev1">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeHvc1(box: VisualSampleEntryBox<"hvc1">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeIden(box: WebVttCueIdBox): IsoBoxWriteView; // @public export function writeImda(box: IdentifiedMediaDataBox): IsoBoxWriteView; // @public export function writeIsoBox(box: IsoBoxStreamable, config?: IsoBoxWriteViewConfig): Uint8Array; // @public export function writeIsoBoxes(boxes: Iterable, config: IsoBoxWriteViewConfig): Uint8Array[]; // @public export function writeKind(box: TrackKindBox): IsoBoxWriteView; // @public export function writeLabl(box: LabelBox): IsoBoxWriteView; // @public export function writeMdat(box: MediaDataBox): IsoBoxWriteView; // @public export function writeMdhd(box: MediaHeaderBox): IsoBoxWriteView; // @public export function writeMehd(box: MovieExtendsHeaderBox): IsoBoxWriteView; // @public export function writeMeta(box: MetaBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeMfhd(box: MovieFragmentHeaderBox): IsoBoxWriteView; // @public export function writeMfro(box: MovieFragmentRandomAccessOffsetBox): IsoBoxWriteView; // @public export function writeMp4a(box: AudioSampleEntryBox<"mp4a">, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeMvhd(box: MovieHeaderBox): IsoBoxWriteView; // @public export function writePayl(box: WebVttCuePayloadBox): IsoBoxWriteView; // @public export function writePrft(box: ProducerReferenceTimeBox): IsoBoxWriteView; // @public export function writePrsl(box: PreselectionGroupBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writePssh(box: ProtectionSystemSpecificHeaderBox): IsoBoxWriteView; // @public export function writeSchm(box: SchemeTypeBox): IsoBoxWriteView; // @public export function writeSdtp(box: SampleDependencyTypeBox): IsoBoxWriteView; // @public export function writeSidx(box: SegmentIndexBox): IsoBoxWriteView; // @public export function writeSkip(box: FreeSpaceBox<"skip">): IsoBoxWriteView; // @public export function writeSmhd(box: SoundMediaHeaderBox): IsoBoxWriteView; // @public export function writeSsix(box: SubsegmentIndexBox): IsoBoxWriteView; // @public export function writeStco(box: ChunkOffsetBox): IsoBoxWriteView; // @public export function writeSthd(box: SubtitleMediaHeaderBox): IsoBoxWriteView; // @public export function writeStsc(box: SampleToChunkBox): IsoBoxWriteView; // @public export function writeStsd(box: SampleDescriptionBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeStss(box: SyncSampleBox): IsoBoxWriteView; // @public export function writeStsz(box: SampleSizeBox): IsoBoxWriteView; // @public export function writeSttg(box: WebVttSettingsBox): IsoBoxWriteView; // @public export function writeStts(box: DecodingTimeToSampleBox): IsoBoxWriteView; // @public export function writeStyp(box: SegmentTypeBox): IsoBoxWriteView; // @public export function writeSubs(box: SubsampleInformationBox): IsoBoxWriteView; // @public export function writeTenc(box: TrackEncryptionBox): IsoBoxWriteView; // @public export function writeTfdt(box: TrackFragmentBaseMediaDecodeTimeBox): IsoBoxWriteView; // @public export function writeTfhd(box: TrackFragmentHeaderBox): IsoBoxWriteView; // @public export function writeTfra(box: TrackFragmentRandomAccessBox): IsoBoxWriteView; // @public export function writeTkhd(box: TrackHeaderBox): IsoBoxWriteView; // @public export function writeTrex(box: TrackExtendsBox): IsoBoxWriteView; // @public export function writeTrun(box: TrackRunBox): IsoBoxWriteView; // @public export function writeUrl(box: DataEntryUrlBox): IsoBoxWriteView; // @public export function writeUrn(box: DataEntryUrnBox): IsoBoxWriteView; // @public export function writeVisualSampleEntryBox(box: VisualSampleEntryBox, config: IsoBoxWriteViewConfig): IsoBoxWriteView; // @public export function writeVlab(box: WebVttSourceLabelBox): IsoBoxWriteView; // @public export function writeVmhd(box: VideoMediaHeaderBox): IsoBoxWriteView; // @public export function writeVttC(box: WebVttConfigurationBox): IsoBoxWriteView; // @public export function writeVtte(_: WebVttEmptySampleBox): IsoBoxWriteView; // (No @packageDocumentation comment for this package) ``` # @svta/cml-request Common Media Request and Response types. ## When to use - Type a segment, manifest, or license request that a player passes through a plugin: `CommonMediaRequest` - Type the response that comes back, with timing and headers: `CommonMediaResponse` - Type the function that performs a request, so that fetch or XMLHttpRequest can implement it: `Requester` - Intercept requests and responses, for example to add CMCD or to read CMSD: `RequestInterceptor`, `ResponseInterceptor` Do not define your own request and response types for a plugin interface. Open an issue for a field that is missing. ## Installation ```bash npm i @svta/cml-request ``` ## Usage ```typescript import type { CommonMediaRequest, CommonMediaResponse, Requester } from "@svta/cml-request"; // A requester adapts an HTTP client to the common request and response types. const fetchRequester: Requester = async (request: CommonMediaRequest): Promise => { const response = await fetch(request.url, { method: request.method, headers: request.headers }); return { request, url: response.url, status: response.status, statusText: response.statusText, }; }; // Pass fetchRequester to a component that accepts a Requester, and call it with a request like this one. const request: CommonMediaRequest = { url: "https://example.com/video/segment_001.m4s", method: "GET", }; ``` ## API Report File for "@svta/cml-request" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { Cmcd } from '@svta/cml-cmcd'; import { HttpRequest } from '@svta/cml-utils'; import { HttpResponse } from '@svta/cml-utils'; import { RequestResponseType } from '@svta/cml-utils'; import { ResourceTiming } from '@svta/cml-utils'; import { ResponseTypeMap } from '@svta/cml-utils'; // @public export type CommonMediaRequest = HttpRequest; // @public export type CommonMediaResponse = HttpResponse; // @public export type Requester = (request: CommonMediaRequest) => Promise; // @public export type RequestInterceptor = (request: CommonMediaRequest) => Promise; export { RequestResponseType } export { ResourceTiming } // @public export type ResponseInterceptor = (response: CommonMediaResponse) => Promise; export { ResponseTypeMap } // (No @packageDocumentation comment for this package) ``` # @svta/cml-structured-field-values RFC8941 Structured Field Values implementation. ## When to use - Decode an RFC 8941 header such as CMCD, CMSD, or Priority into items, lists, or dictionaries: `decodeSfDict`, `decodeSfList`, `decodeSfItem` - Encode a dictionary, a list, or an item as a structured field header: `encodeSfDict`, `encodeSfList`, `encodeSfItem` - Represent an item with parameters or a token: `SfItem`, `SfToken` Do not write your own structured field tokenizer or serializer. Open an issue for an RFC 8941 form that fails to parse. ## Installation ```bash npm i @svta/cml-structured-field-values ``` ## Usage ```typescript import { encodeSfDict } from "@svta/cml-structured-field-values"; const dict = encodeSfDict({ a: 1, b: false, c: "x", d: Symbol.for("y"), e: new Uint8Array([1, 2, 3]), }); console.log(dict); // a=1, b=?0, c="x", d=y, e=:AQID: ``` ## API Report File for "@svta/cml-structured-field-values" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @public export function decodeSfDict(input: string, options?: SfDecodeOptions): SfDictionary; // @public export function decodeSfItem(input: string, options?: SfDecodeOptions): SfItem; // @public export function decodeSfList(input: string, options?: SfDecodeOptions): SfMember[]; // @public export function encodeSfDict(value: Record | Map, options?: SfEncodeOptions): string; // @public export function encodeSfItem(value: SfItem): string; // @public export function encodeSfItem(value: SfBareItem, params?: SfParameters): string; // @public export function encodeSfList(value: SfMember[], options?: SfEncodeOptions): string; // @internal (undocumented) export function parseBareItem(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseBoolean(src: string): ParsedValue; // @internal (undocumented) export function parseByteSequence(src: string): ParsedValue; // @internal (undocumented) export function parseDate(src: string): ParsedValue; // @internal (undocumented) export function parseDict(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export type ParsedValue = { value: T; src: string; }; // @internal (undocumented) export function parseError(src: any, type: string, cause?: any): Error; // @internal (undocumented) export function parseInnerList(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseIntegerOrDecimal(src: string): ParsedValue; // @internal (undocumented) export function parseItem(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseItemOrInnerList(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseKey(src: string): ParsedValue; // @internal (undocumented) export function parseList(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseParameters(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function parseString(src: string): ParsedValue; // @internal (undocumented) export function parseToken(src: string, options?: SfDecodeOptions): ParsedValue; // @internal (undocumented) export function serializeBareItem(value: any): string; // @internal (undocumented) export function serializeBoolean(value: boolean): string; // @internal (undocumented) export function serializeByteSequence(value: Uint8Array): string; // @internal (undocumented) export function serializeDate(value: Date): string; // @internal (undocumented) export function serializeDecimal(value: number): string; // @internal (undocumented) export function serializeDict(dict: Record | Map, options?: SfEncodeOptions): string; // @internal (undocumented) export function serializeError(src: any, type: string, cause?: any): Error; // @internal (undocumented) export function serializeInnerList(value: SfInnerList): string; // @internal (undocumented) export function serializeInnerList(value: SfItem[] | SfBareItem[], params?: SfParameters): string; // @internal (undocumented) export function serializeInteger(value: number): string; // @internal (undocumented) export function serializeItem(value: SfItem | SfBareItem): string; // @internal (undocumented) export function serializeKey(value: string): string; // @internal (undocumented) export function serializeList(list: SfMember[], options?: SfEncodeOptions): string; // @internal (undocumented) export function serializeParams(params?: Record): string; // @internal (undocumented) export function serializeString(value: string): string; // @internal (undocumented) export function serializeToken(token: symbol | SfToken): string; // @public export type SfBareItem = string | Uint8Array | boolean | number | symbol | Date | SfToken; // @public export type SfDecodeOptions = { useSymbol?: boolean; }; // @public export type SfDictionary = Record | Map; // @public export type SfEncodeOptions = { whitespace?: boolean; }; // @public export type SfInnerList = { value: SfItem[] | SfBareItem[]; params: SfParameters; }; // @public export class SfItem> { constructor(value: any, params?: P); params?: P; value: V; } // @public export type SfMember = SfItem | SfInnerList | SfBareItem; // @public export type SfParameters = Record; // @public export class SfToken { constructor(description: string); // (undocumented) description: string; } // @public export function symbolToStr(symbol: symbol | SfToken): string; // (No @packageDocumentation comment for this package) ``` # @svta/cml-throughput Throughput estimation functionality. ## When to use - Estimate throughput from segment downloads with an exponentially weighted moving average: `EwmaEstimator` - Estimate throughput with a harmonic mean, an arithmetic mean, or a zero-lag EWMA: `HarmonicMeanEstimator`, `ArithmeticMeanEstimator`, `ZlemaEstimator` - Implement a custom estimator behind the shared interface: `ThroughputEstimator` - Tune the fast and slow half-lives of the EWMA: `EwmaEstimatorOptions` Do not write your own EWMA or harmonic mean for bandwidth estimation. Open an issue for an estimator type that is missing. ## Installation ```bash npm i @svta/cml-throughput ``` ## Usage ```typescript import { EwmaEstimator } from "@svta/cml-throughput"; const estimator = new EwmaEstimator({ fastHalfLife: 2, slowHalfLife: 5 }); // 500 bps estimator.sample({ startTime: 0, encodedBodySize: 125, duration: 2000, }); // 250 bps estimator.sample({ startTime: 2000, encodedBodySize: 125, duration: 4000, }); console.log(estimator.getEstimate()); // 285 ``` ## Samples A sample needs an `encodedBodySize` above 0 and a `duration` above 0. The estimators ignore every other sample, and the estimate does not change. A player can pass `PerformanceResourceTiming` entries as samples. For a cross-origin segment, the browser decides whether the entry has a body size: | Browser | Body size of a cross-origin response | | --- | --- | | Chrome | Exposed when the response has a `Timing-Allow-Origin` header for the page origin. Chrome plans the Firefox rule for version 158. | | Firefox 129 and later | Exposed when the request used CORS. A `no-cors` request reports 0, even with the header. | | Safari | 0 for every cross-origin response. | An entry with a body size of 0 is ignored, so the estimate does not change. The byte count of the loader works in every browser. Build the sample from that count when the player has it. ## API Report File for "@svta/cml-throughput" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { ResourceTiming } from '@svta/cml-utils'; // @public export class ArithmeticMeanEstimator implements ThroughputEstimator { // (undocumented) getEstimate(): number; // (undocumented) sample(sample: ResourceTiming): void; } // @public export class Ewma { constructor(alpha: number); getEstimate(): number; getTotalDuration(): number; sample(weight: number, value: number): void; } // @public export class EwmaEstimator implements ThroughputEstimator { constructor(options: EwmaEstimatorOptions); // (undocumented) getEstimate(): number; // (undocumented) sample(sample: ResourceTiming): void; } // @public export type EwmaEstimatorOptions = { defaultEstimate?: number; minTotalBytes?: number; minBodySize?: number; fastHalfLife: number; slowHalfLife: number; }; // @public export class HarmonicMeanEstimator implements ThroughputEstimator { // (undocumented) getEstimate(): number; // (undocumented) sample(sample: ResourceTiming): void; } // @public export type ThroughputEstimator = { sample(sample: ResourceTiming): void; getEstimate(): number; }; // @public export class ZlemaEstimator implements ThroughputEstimator { // (undocumented) getEstimate(): number; // (undocumented) sample(sample: ResourceTiming): void; } // (No @packageDocumentation comment for this package) ``` # @svta/cml-utils Common utility functions for media processing. ## When to use - Convert between hex, base64, UUID strings, and byte arrays: `hexToArrayBuffer`, `arrayBufferToHex`, `arrayBufferToUuid`, `uuidToArrayBuffer`, `decodeBase64`, `encodeBase64` - Encode or decode UTF-8 and UTF-16 text: `encodeText`, `decodeText`, `Encoding` - Resolve a base URL or make a URL relative to another URL: `getBaseUrl`, `urlToRelativePath` - Compute bandwidth in bits per second from a byte count and a duration: `getBandwidthBps` - Unescape HTML entities in a manifest or a caption string: `unescapeHtml` - Generate a UUID for a session or a content id: `uuid` Do not write your own hex, base64, or UUID conversion. Open an issue for a conversion that is missing. ## Installation ```bash npm i @svta/cml-utils ``` ## Usage ```typescript import { stringToUint16, hexToArrayBuffer, decodeBase64, arrayBufferToUuid, arrayBufferToHex, unescapeHtml, } from "@svta/cml-utils"; // Unescape HTML entities console.log(unescapeHtml(""<Hello&World>"")); // "" // Convert string to Uint16Array console.log(stringToUint16("hello world")); // Uint16Array(11) [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // Convert hex string to ArrayBuffer const hex = "a0564af760c2d94b9610c7ae70d5d970"; const arrayBuffer = hexToArrayBuffer(hex); console.log(new Uint8Array(arrayBuffer)); // Uint8Array(16) [160, 86, 74, 247, 96, 194, 217, 75, 150, 16, 199, 174, 112, 213, 217, 112] // Decode base64 console.log(decodeBase64("SGVsbG8gV29ybGQ=")); // Uint8Array(11) [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] // Convert ArrayBuffer to UUID const data = new Uint8Array([ 160, 86, 74, 247, 96, 194, 217, 75, 150, 16, 199, 174, 112, 213, 217, 112, ]); console.log(arrayBufferToUuid(data.buffer)); // a0564af7-60c2-d94b-9610-c7ae70d5d970 // Convert ArrayBuffer to hex string console.log(arrayBufferToHex(data.buffer)); // a0564af760c2d94b9610c7ae70d5d970 ``` ## API Report File for "@svta/cml-utils" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts // @public export function arrayBufferToHex(buffer: ArrayBuffer): string; // @public export function arrayBufferToUuid(buffer: ArrayBuffer): string; // @public @deprecated export const base64decode: typeof decodeBase64; // @public @deprecated export const base64encode: typeof encodeBase64; // @public export function convertUint8ToUint16(input: Uint8Array): Uint16Array; // @public export function decodeBase64(str: string): Uint8Array; // @public export function decodeText(data: ArrayBufferLike | ArrayBufferView, options?: DecodeTextOptions): string; // @public export type DecodeTextOptions = { encoding?: Encoding; }; // @public export type DeepReadonly = T extends ((...args: never[]) => unknown) ? T : T extends readonly (infer U)[] ? readonly DeepReadonly[] : T extends object ? { readonly [P in keyof T]: DeepReadonly; } : T; // @public export function encodeBase64(binary: Uint8Array): string; // @public export function encodeText(data: string): Uint8Array; // @public export const Encoding: { readonly UTF8: typeof UTF_8; readonly UTF16: typeof UTF_16; readonly UTF16BE: typeof UTF_16_BE; readonly UTF16LE: typeof UTF_16_LE; }; // @public export type Encoding = ValueOf; // @public export type ExclusiveRecord = { [P in K]: Record & Partial, never>> extends (infer O) ? { [Q in keyof O]: O[Q]; } : never; }[K]; // @public export function getBandwidthBps(sample: ResourceTiming): number; // @public export function getBaseUrl(fullUrl: string | URL): string; // @public export function hexToArrayBuffer(hex: string): ArrayBuffer; // @public export type HttpRequest = { url: string; method?: string; body?: BodyInit; responseType?: RequestResponseType; headers?: Record; credentials?: RequestCredentials; mode?: RequestMode; timeout?: number; customData?: D; }; // @public export type HttpResponse = { request: R; url?: string; redirected?: boolean; status?: number; statusText?: string; type?: string; headers?: Record; data?: ResponseTypeMap; resourceTiming?: ResourceTiming; }; // @public export function isArrayBufferLike(value: unknown): value is ArrayBufferLike; // @public @deprecated type Request_2 = HttpRequest; export { Request_2 as Request } // @public export const RequestResponseType: { readonly TEXT: "text"; readonly JSON: "json"; readonly BLOB: "blob"; readonly ARRAY_BUFFER: "arrayBuffer"; readonly DOCUMENT: "document"; readonly STREAM: "stream"; }; // @public (undocumented) export type RequestResponseType = ValueOf; // @public export type ResourceTiming = { startTime: number; encodedBodySize: number; responseStart?: number; duration: number; }; // @public export type ResponseTypeMap = T extends "json" ? any : T extends "text" ? string : T extends "blob" ? Blob : T extends "arrayBuffer" ? ArrayBuffer : T extends "document" ? XmlNode : T extends "stream" ? ReadableStream : unknown; // @public export function roundToEven(value: number, precision: number): number; // @public export function stringToUint16(str: string): Uint16Array; // @public export type TypedResult = { type: T; data: D; }; // @public export function unescapeHtml(text: string): string; // @public export function urlToRelativePath(url: string, base: string): string; // @public export const UTF_16 = "utf-16"; // @public export const UTF_16_BE = "utf-16be"; // @public export const UTF_16_LE = "utf-16le"; // @public export const UTF_8 = "utf-8"; // @public export function uuid(): string; // @public export function uuidToArrayBuffer(uuid: string): ArrayBuffer; // @public export type ValueOf = T[keyof T]; // @public export type ValueOrArray = T | T[]; // @public export type XmlNode = { nodeName: string; nodeValue: string | null; attributes: Record; childNodes: XmlNode[]; prefix?: string | null; localName?: string; parentElement?: XmlNode | null; }; // (No @packageDocumentation comment for this package) ``` # @svta/cml-webvtt WebVTT parsing and rendering functionality. ## When to use - Parse a WebVTT file or segment into cues, regions, and styles: `parseWebVtt`, `WebVttParser` - Parse WebVTT chunks as the bytes arrive from a stream: `WebVttTransformStream`, `WebVttTransformer` - Read the X-TIMESTAMP-MAP header of an HLS WebVTT segment: `WebVttParser`, `TimestampMap` - Create native VTTCue and VTTRegion objects from parsed cues: `toVttCue`, `toVttRegion` - Build cues and regions without a browser, for tests or servers: `createWebVttCue`, `createWebVttRegion` Do not write your own WebVTT tokenizer, cue timing parser, or timestamp map handling. Open an issue for a cue setting that is missing. ## Installation ```bash npm i @svta/cml-webvtt ``` ## Usage ```typescript import { parseWebVtt } from "@svta/cml-webvtt"; const vtt = `WEBVTT\n\nREGION\nid:test\n\nSTYLE\n::cue {}\n\nCUE_1\n00:00:00.000 --> 00:00:35.000\nWebVTT Sample\n`; const { cues, errors, regions, styles } = await parseWebVtt(vtt); console.log(cues[0].id, cues[0].text); // CUE_1 WebVTT Sample console.log(errors.length, regions[0].id, styles.length); // 0 test 1 ``` ## API Report File for "@svta/cml-webvtt" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { TypedResult } from '@svta/cml-utils'; import { ValueOf } from '@svta/cml-utils'; // @public export function createWebVttCue(): WebVttCue; // @public export function createWebVttRegion(): WebVttRegion; // @public export function parseWebVtt(text: string, options?: WebVttParserOptions): Promise; // @public export type TimestampMap = { MPEGTS: number; LOCAL: number; }; // @public export function toVttCue(cue: WebVttCue): VTTCue; // @public export function toVttRegion(region: WebVttRegion): VTTRegion; // @public export type WebVttCue = { id: string; startTime: number; endTime: number; pauseOnExit: boolean; text: string; align: AlignSetting; region: WebVttRegion | null; vertical: DirectionSetting; snapToLines: boolean; line: LineAndPositionSetting; lineAlign: LineAlignSetting; position: LineAndPositionSetting; positionAlign: PositionAlignSetting; size: number; }; // @public export type WebVttCueFactory = () => WebVttCue; // @public export type WebVttCueResult = TypedResult<"cue", WebVttCue>; // @public export type WebVttErrorResult = TypedResult<"error", WebVttParsingError>; // @public export class WebVttParser { constructor(options?: WebVttParserOptions); flush(): WebVttParser; oncue?: (cue: WebVttCue) => void; onflush?: () => void; onparsingerror?: (error: WebVttParsingError) => void; onregion?: (region: WebVttRegion) => void; onstyle?: (style: string) => void; ontimestampmap?: (timestampMap: TimestampMap) => void; parse(data?: string, reuseCue?: boolean): WebVttParser; } // @public export type WebVttParseResult = { cues: WebVttCue[]; regions: WebVttRegion[]; styles: string[]; errors: WebVttParsingError[]; }; // @public export type WebVttParserOptions = { useDomTypes?: boolean; createCue?: WebVttCueFactory; createRegion?: WebVttRegionFactory; }; // @public export class WebVttParsingError extends Error { constructor(message: string); } // @public export type WebVttRegion = { id: string; width: number; lines: number; regionAnchorX: number; regionAnchorY: number; viewportAnchorX: number; viewportAnchorY: number; scroll: ScrollSetting; }; // @public export type WebVttRegionFactory = () => WebVttRegion; // @public export type WebVttRegionResult = TypedResult<"region", WebVttRegion>; // @public export type WebVttResult = WebVttCueResult | WebVttRegionResult | WebVttTimestampMapResult | WebVttStyleResult | WebVttErrorResult; // @public export const WebVttResultType: { readonly CUE: "cue"; readonly REGION: "region"; readonly TIMESTAMP_MAP: "timestampmap"; readonly STYLE: "style"; readonly ERROR: "error"; }; // @public (undocumented) export type WebVttResultType = ValueOf; // @public export type WebVttStyleResult = TypedResult<"style", string>; // @public export type WebVttTimestampMapResult = TypedResult<"timestampmap", TimestampMap>; // @public export class WebVttTransformer { constructor(); flush(controller: TransformStreamDefaultController): void; transform(chunk: string, controller: TransformStreamDefaultController): void; } // Warning: (ae-forgotten-export) The symbol "TransformStreamBase" needs to be exported by the entry point index.d.ts // // @public export class WebVttTransformStream extends TransformStreamBase { constructor(writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy); } // (No @packageDocumentation comment for this package) ``` # @svta/cml-xml XML parsing utilities. ## When to use - Parse a DASH MPD or another XML document into a node tree: `parseXml` - Find elements by name anywhere in the tree: `getElementsByName` - Serialize a node tree back to an XML string: `serializeXml` - Keep whitespace or comments, or start the parse at an offset: `XmlParseOptions` Do not write your own XML tokenizer for manifests. Open an issue for an XML form that fails to parse. ## Installation ```bash npm i @svta/cml-xml ``` ## Usage ```typescript import { parseXml } from "@svta/cml-xml"; const obj = parseXml( ` text content ` ); const root = obj.childNodes[0]; console.log(root.nodeName); // root console.log(root.childNodes[0].nodeName, root.childNodes[0].childNodes[0].nodeValue); // child text console.log(root.childNodes[1].nodeName, root.childNodes[1].prefix, root.childNodes[1].localName); // ns:tag ns tag ``` ## API Report File for "@svta/cml-xml" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts import { XmlNode } from '@svta/cml-utils'; // @public export function getElementsByName(node: XmlNode, name: string, found?: XmlNode[]): XmlNode[]; // @public export function parseXml(input: string, options?: XmlParseOptions): XmlNode; // @public export function serializeXml(xml: XmlNode): string; export { XmlNode } // @public export type XmlParseOptions = { pos?: number; keepWhitespace?: boolean; keepComments?: boolean; includeParentElement?: boolean; }; // (No @packageDocumentation comment for this package) ```