@openmoq/playback 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +18 -2
  3. package/dist/adaptive-tolerance.d.ts +128 -0
  4. package/dist/adaptive-tolerance.d.ts.map +1 -0
  5. package/dist/adaptive-tolerance.js +339 -0
  6. package/dist/adaptive-tolerance.js.map +1 -0
  7. package/dist/bandwidth-estimator.d.ts +49 -0
  8. package/dist/bandwidth-estimator.d.ts.map +1 -0
  9. package/dist/bandwidth-estimator.js +77 -0
  10. package/dist/bandwidth-estimator.js.map +1 -0
  11. package/dist/browser/index.global.js +2813 -0
  12. package/dist/browser/index.global.js.map +1 -0
  13. package/dist/browser/index.js +2788 -0
  14. package/dist/browser/index.js.map +1 -0
  15. package/dist/buffer-based-controller.d.ts +87 -0
  16. package/dist/buffer-based-controller.d.ts.map +1 -0
  17. package/dist/buffer-based-controller.js +85 -0
  18. package/dist/buffer-based-controller.js.map +1 -0
  19. package/dist/decoder-state.d.ts +103 -0
  20. package/dist/decoder-state.d.ts.map +1 -0
  21. package/dist/decoder-state.js +145 -0
  22. package/dist/decoder-state.js.map +1 -0
  23. package/dist/gap-detector.d.ts +83 -0
  24. package/dist/gap-detector.d.ts.map +1 -0
  25. package/dist/gap-detector.js +129 -0
  26. package/dist/gap-detector.js.map +1 -0
  27. package/dist/index.d.ts +33 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +33 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/jitter-buffer.d.ts +73 -0
  32. package/dist/jitter-buffer.d.ts.map +1 -0
  33. package/dist/jitter-buffer.js +216 -0
  34. package/dist/jitter-buffer.js.map +1 -0
  35. package/dist/keyframe-validator.d.ts +26 -0
  36. package/dist/keyframe-validator.d.ts.map +1 -0
  37. package/dist/keyframe-validator.js +170 -0
  38. package/dist/keyframe-validator.js.map +1 -0
  39. package/dist/pipeline.d.ts +215 -0
  40. package/dist/pipeline.d.ts.map +1 -0
  41. package/dist/pipeline.js +852 -0
  42. package/dist/pipeline.js.map +1 -0
  43. package/dist/recovery.d.ts +166 -0
  44. package/dist/recovery.d.ts.map +1 -0
  45. package/dist/recovery.js +176 -0
  46. package/dist/recovery.js.map +1 -0
  47. package/dist/render-scheduler.d.ts +50 -0
  48. package/dist/render-scheduler.d.ts.map +1 -0
  49. package/dist/render-scheduler.js +84 -0
  50. package/dist/render-scheduler.js.map +1 -0
  51. package/dist/startup-buffer.d.ts +68 -0
  52. package/dist/startup-buffer.d.ts.map +1 -0
  53. package/dist/startup-buffer.js +107 -0
  54. package/dist/startup-buffer.js.map +1 -0
  55. package/dist/sync.d.ts +234 -0
  56. package/dist/sync.d.ts.map +1 -0
  57. package/dist/sync.js +343 -0
  58. package/dist/sync.js.map +1 -0
  59. package/dist/types.d.ts +229 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +12 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +56 -3
@@ -0,0 +1,87 @@
1
+ /**
2
+ * BufferBasedController — hybrid ABR using buffer depth as the primary
3
+ * signal and bandwidth estimation as a secondary corroboration signal.
4
+ *
5
+ * Buffer depth directly measures stall risk (the thing we care about).
6
+ * Bandwidth estimation measures throughput (a proxy for stall risk that
7
+ * can be noisy on bursty networks). Combining them:
8
+ *
9
+ * buffer < LOW → downshift immediately (emergency, bandwidth irrelevant)
10
+ * buffer in [LOW, HIGH] → hold steady (buffer is fine, don't react to noise)
11
+ * buffer > HIGH AND bandwidth > 2.25× next-higher → upshift (both agree)
12
+ * buffer > HIGH AND bandwidth insufficient → hold (don't burn buffer)
13
+ *
14
+ * This avoids the EWMA over-reaction problem (bursty delivery tanks the
15
+ * estimate but buffer is healthy) while still enabling proactive upshift
16
+ * when conditions genuinely support it.
17
+ *
18
+ * @module
19
+ */
20
+ import type { ClockSource } from './types.js';
21
+ /** One entry in the ABR quality ladder. */
22
+ export interface AbrTrack {
23
+ readonly name: string;
24
+ readonly bitrateKbps: number;
25
+ }
26
+ /** Configuration for the buffer-based controller. */
27
+ export interface BufferBasedConfig {
28
+ readonly clock: ClockSource;
29
+ /** Quality ladder sorted highest-bitrate-first (index 0 = best). */
30
+ readonly ladder: readonly AbrTrack[];
31
+ /** Starting index into the ladder. */
32
+ readonly initialIndex: number;
33
+ /** Buffer depth below this → emergency downshift (µs). */
34
+ readonly lowThresholdUs: number;
35
+ /** Buffer depth above this → consider upshift (µs). */
36
+ readonly highThresholdUs: number;
37
+ /** Bandwidth must exceed this multiple of the target track's bitrate
38
+ * for an upshift to be approved. Default 2.25. */
39
+ readonly upshiftBandwidthMultiple?: number;
40
+ }
41
+ /** Input signals for an ABR evaluation. */
42
+ export interface AbrSignals {
43
+ /** Current buffer depth in microseconds. */
44
+ readonly bufferDepthUs: number;
45
+ /** EWMA bandwidth estimate in kbps (0 = unknown). */
46
+ readonly bandwidthEstimateKbps: number;
47
+ }
48
+ /** ABR decision. */
49
+ export interface AbrDecision {
50
+ readonly action: 'downshift' | 'upshift' | 'hold';
51
+ /** Target ladder index (only set for downshift/upshift). */
52
+ readonly targetIndex?: number;
53
+ }
54
+ export declare class BufferBasedController {
55
+ private readonly ladder;
56
+ readonly lowThresholdUs: number;
57
+ /** Kept for future temporal-buffer mode (playout buffer, not jitter buffer). */
58
+ readonly highThresholdUs: number;
59
+ private readonly upshiftMultiple;
60
+ private currentIndex;
61
+ constructor(config: BufferBasedConfig);
62
+ /** Current ladder index. */
63
+ get index(): number;
64
+ /** Current track. */
65
+ get currentTrack(): AbrTrack | undefined;
66
+ /**
67
+ * Evaluate buffer depth + bandwidth and return an ABR decision.
68
+ *
69
+ * Called periodically (e.g., every tick or every group arrival).
70
+ */
71
+ /**
72
+ * Evaluate buffer depth + bandwidth and return an ABR recommendation.
73
+ *
74
+ * Does NOT mutate internal state — the caller must call
75
+ * {@link commitDownshift} or {@link commitUpshift} after a
76
+ * confirmed switch. This prevents index drift when async switches
77
+ * fail or are aborted.
78
+ */
79
+ evaluate(signals: AbrSignals): AbrDecision;
80
+ /** Confirm a downshift — advances index to the lower track. */
81
+ commitDownshift(): void;
82
+ /** Confirm an upshift — moves index to the higher track. */
83
+ commitUpshift(): void;
84
+ /** Reset to a specific ladder index (e.g., after manual selection). */
85
+ setIndex(index: number): void;
86
+ }
87
+ //# sourceMappingURL=buffer-based-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer-based-controller.d.ts","sourceRoot":"","sources":["../src/buffer-based-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,2CAA2C;AAC3C,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;IACrC,sCAAsC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;uDACmD;IACnD,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;CAC9C;AAED,2CAA2C;AAC3C,MAAM,WAAW,UAAU;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,qDAAqD;IACrD,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;CAC1C;AAED,oBAAoB;AACpB,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;IAClD,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,qBAAa,qBAAqB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,YAAY,CAAS;gBAEjB,MAAM,EAAE,iBAAiB;IAQrC,4BAA4B;IAC5B,IAAI,KAAK,IAAI,MAAM,CAA8B;IAEjD,qBAAqB;IACrB,IAAI,YAAY,IAAI,QAAQ,GAAG,SAAS,CAA2C;IAEnF;;;;OAIG;IACH;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,WAAW;IAsB1C,+DAA+D;IAC/D,eAAe,IAAI,IAAI;IAIvB,4DAA4D;IAC5D,aAAa,IAAI,IAAI;IAIrB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAGhC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * BufferBasedController — hybrid ABR using buffer depth as the primary
3
+ * signal and bandwidth estimation as a secondary corroboration signal.
4
+ *
5
+ * Buffer depth directly measures stall risk (the thing we care about).
6
+ * Bandwidth estimation measures throughput (a proxy for stall risk that
7
+ * can be noisy on bursty networks). Combining them:
8
+ *
9
+ * buffer < LOW → downshift immediately (emergency, bandwidth irrelevant)
10
+ * buffer in [LOW, HIGH] → hold steady (buffer is fine, don't react to noise)
11
+ * buffer > HIGH AND bandwidth > 2.25× next-higher → upshift (both agree)
12
+ * buffer > HIGH AND bandwidth insufficient → hold (don't burn buffer)
13
+ *
14
+ * This avoids the EWMA over-reaction problem (bursty delivery tanks the
15
+ * estimate but buffer is healthy) while still enabling proactive upshift
16
+ * when conditions genuinely support it.
17
+ *
18
+ * @module
19
+ */
20
+ export class BufferBasedController {
21
+ ladder;
22
+ lowThresholdUs;
23
+ /** Kept for future temporal-buffer mode (playout buffer, not jitter buffer). */
24
+ highThresholdUs;
25
+ upshiftMultiple;
26
+ currentIndex;
27
+ constructor(config) {
28
+ this.ladder = config.ladder;
29
+ this.lowThresholdUs = config.lowThresholdUs;
30
+ this.highThresholdUs = config.highThresholdUs;
31
+ this.upshiftMultiple = config.upshiftBandwidthMultiple ?? 2.25;
32
+ this.currentIndex = config.initialIndex;
33
+ }
34
+ /** Current ladder index. */
35
+ get index() { return this.currentIndex; }
36
+ /** Current track. */
37
+ get currentTrack() { return this.ladder[this.currentIndex]; }
38
+ /**
39
+ * Evaluate buffer depth + bandwidth and return an ABR decision.
40
+ *
41
+ * Called periodically (e.g., every tick or every group arrival).
42
+ */
43
+ /**
44
+ * Evaluate buffer depth + bandwidth and return an ABR recommendation.
45
+ *
46
+ * Does NOT mutate internal state — the caller must call
47
+ * {@link commitDownshift} or {@link commitUpshift} after a
48
+ * confirmed switch. This prevents index drift when async switches
49
+ * fail or are aborted.
50
+ */
51
+ evaluate(signals) {
52
+ // Emergency: buffer critically low → recommend downshift
53
+ if (signals.bufferDepthUs < this.lowThresholdUs) {
54
+ if (this.currentIndex < this.ladder.length - 1) {
55
+ return { action: 'downshift', targetIndex: this.currentIndex + 1 };
56
+ }
57
+ return { action: 'hold' }; // already at lowest
58
+ }
59
+ // Upshift: buffer must be above the high threshold — asymmetric
60
+ // hysteresis so upshift is much harder to trigger than downshift.
61
+ // AND bandwidth must corroborate.
62
+ if (this.currentIndex > 0 && signals.bufferDepthUs >= this.highThresholdUs) {
63
+ const higherTrack = this.ladder[this.currentIndex - 1];
64
+ if (signals.bandwidthEstimateKbps > higherTrack.bitrateKbps * this.upshiftMultiple) {
65
+ return { action: 'upshift', targetIndex: this.currentIndex - 1 };
66
+ }
67
+ }
68
+ return { action: 'hold' };
69
+ }
70
+ /** Confirm a downshift — advances index to the lower track. */
71
+ commitDownshift() {
72
+ if (this.currentIndex < this.ladder.length - 1)
73
+ this.currentIndex++;
74
+ }
75
+ /** Confirm an upshift — moves index to the higher track. */
76
+ commitUpshift() {
77
+ if (this.currentIndex > 0)
78
+ this.currentIndex--;
79
+ }
80
+ /** Reset to a specific ladder index (e.g., after manual selection). */
81
+ setIndex(index) {
82
+ this.currentIndex = Math.max(0, Math.min(index, this.ladder.length - 1));
83
+ }
84
+ }
85
+ //# sourceMappingURL=buffer-based-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buffer-based-controller.js","sourceRoot":"","sources":["../src/buffer-based-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAyCH,MAAM,OAAO,qBAAqB;IACb,MAAM,CAAsB;IACpC,cAAc,CAAS;IAChC,gFAAgF;IACvE,eAAe,CAAS;IAChB,eAAe,CAAS;IACjC,YAAY,CAAS;IAE7B,YAAY,MAAyB;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;QAC5C,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,CAAC;QAC9C,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,wBAAwB,IAAI,IAAI,CAAC;QAC/D,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IAC5C,CAAC;IAED,4BAA4B;IAC5B,IAAI,KAAK,KAAa,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAEjD,qBAAqB;IACrB,IAAI,YAAY,KAA2B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAEnF;;;;OAIG;IACH;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAmB;QACxB,yDAAyD;QACzD,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YAC9C,IAAI,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YACvE,CAAC;YACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,oBAAoB;QACnD,CAAC;QAED,gEAAgE;QAChE,kEAAkE;QAClE,kCAAkC;QAClC,IAAI,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAE,CAAC;YACxD,IAAI,OAAO,CAAC,qBAAqB,GAAG,WAAW,CAAC,WAAW,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;gBACjF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YACrE,CAAC;QACL,CAAC;QAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,+DAA+D;IAC/D,eAAe;QACX,IAAI,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,YAAY,EAAE,CAAC;IACxE,CAAC;IAED,4DAA4D;IAC5D,aAAa;QACT,IAAI,IAAI,CAAC,YAAY,GAAG,CAAC;YAAE,IAAI,CAAC,YAAY,EAAE,CAAC;IACnD,CAAC;IAED,uEAAuE;IACvE,QAAQ,CAAC,KAAa;QAClB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;CACJ"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Decoder lifecycle FSM.
3
+ *
4
+ * Models the decoder's state without touching actual WebCodecs APIs.
5
+ * Tracks whether a keyframe is needed before delta frames can be decoded
6
+ * (after startup or after a gap).
7
+ *
8
+ * Audio is treated as independently decodable (Opus/AAC-LC frames are
9
+ * self-contained) — no keyframe wait needed.
10
+ *
11
+ * ```
12
+ * IDLE ──configure──► NEEDS_KEYFRAME ──keyframe──► DECODING ──gap──► NEEDS_KEYFRAME
13
+ * │
14
+ * END_OF_TRACK
15
+ * │
16
+ * ▼
17
+ * ENDED
18
+ * ```
19
+ *
20
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking.independent)
21
+ * @module
22
+ */
23
+ import type { VideoChunkInit, AudioChunkInit, VideoFrameMarking } from '@openmoq/loc';
24
+ /**
25
+ * Decoder states.
26
+ */
27
+ export declare const DecoderState: {
28
+ /** Not yet configured. */
29
+ readonly IDLE: "idle";
30
+ /** Configured but waiting for a keyframe. */
31
+ readonly NEEDS_KEYFRAME: "needs_keyframe";
32
+ /** Actively decoding. */
33
+ readonly DECODING: "decoding";
34
+ /** Track ended. */
35
+ readonly ENDED: "ended";
36
+ };
37
+ export type DecoderStateValue = (typeof DecoderState)[keyof typeof DecoderState];
38
+ /**
39
+ * Decision for each incoming chunk.
40
+ */
41
+ export type FrameDecision = {
42
+ readonly action: 'decode';
43
+ readonly chunk: VideoChunkInit | AudioChunkInit;
44
+ } | {
45
+ readonly action: 'skip';
46
+ readonly reason: string;
47
+ } | {
48
+ readonly action: 'configure';
49
+ readonly config: Uint8Array;
50
+ } | {
51
+ readonly action: 'flush';
52
+ } | {
53
+ readonly action: 'reset';
54
+ readonly reason: string;
55
+ };
56
+ /**
57
+ * Decoder lifecycle FSM.
58
+ *
59
+ * @see draft-ietf-moq-loc-01 §4.2 (decode order)
60
+ */
61
+ export declare class DecoderStateMachine {
62
+ private _state;
63
+ private readonly _mediaType;
64
+ private lastConfig;
65
+ constructor(mediaType: 'video' | 'audio');
66
+ get state(): DecoderStateValue;
67
+ get mediaType(): 'video' | 'audio';
68
+ /**
69
+ * Provide codec configuration.
70
+ *
71
+ * If the config is identical to the last one, skip reconfiguration
72
+ * (stay in current state). This avoids resetting to NEEDS_KEYFRAME
73
+ * on every group boundary when the server sends videoConfig on
74
+ * every group's first object with unchanged codec parameters.
75
+ *
76
+ * Transitions IDLE/DECODING → NEEDS_KEYFRAME (video) or DECODING (audio)
77
+ * only when the config actually changes.
78
+ */
79
+ configure(config: Uint8Array): FrameDecision;
80
+ /** Byte-level config comparison. */
81
+ private configEquals;
82
+ /**
83
+ * Process a video chunk.
84
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking)
85
+ */
86
+ processVideoChunk(chunk: VideoChunkInit, marking: VideoFrameMarking | undefined): FrameDecision;
87
+ /**
88
+ * Process an audio chunk. Audio codecs (Opus, AAC-LC) produce
89
+ * independently decodable frames — no keyframe gating needed.
90
+ */
91
+ processAudioChunk(chunk: AudioChunkInit): FrameDecision;
92
+ /**
93
+ * Signal a gap — decoder needs to wait for next keyframe (video only).
94
+ * Audio: stays in DECODING since all chunks are independently decodable.
95
+ */
96
+ notifyGap(): FrameDecision;
97
+ /**
98
+ * Signal track ended.
99
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
100
+ */
101
+ notifyTrackEnded(): FrameDecision;
102
+ }
103
+ //# sourceMappingURL=decoder-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder-state.d.ts","sourceRoot":"","sources":["../src/decoder-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItF;;GAEG;AACH,eAAO,MAAM,YAAY;IACrB,0BAA0B;;IAE1B,6CAA6C;;IAE7C,yBAAyB;;IAEzB,mBAAmB;;CAEb,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAIjF;;GAEG;AACH,MAAM,MAAM,aAAa,GACnB;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAAA;CAAE,GAC9E;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAI5D;;;;GAIG;AACH,qBAAa,mBAAmB;IAC5B,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,UAAU,CAA2B;gBAEjC,SAAS,EAAE,OAAO,GAAG,OAAO;IAIxC,IAAI,KAAK,IAAI,iBAAiB,CAE7B;IAED,IAAI,SAAS,IAAI,OAAO,GAAG,OAAO,CAEjC;IAED;;;;;;;;;;OAUG;IACH,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa;IAiB5C,oCAAoC;IACpC,OAAO,CAAC,YAAY;IAQpB;;;OAGG;IACH,iBAAiB,CACb,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,iBAAiB,GAAG,SAAS,GACvC,aAAa;IAmBhB;;;OAGG;IACH,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,aAAa;IAUvD;;;OAGG;IACH,SAAS,IAAI,aAAa;IAS1B;;;OAGG;IACH,gBAAgB,IAAI,aAAa;CAIpC"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Decoder lifecycle FSM.
3
+ *
4
+ * Models the decoder's state without touching actual WebCodecs APIs.
5
+ * Tracks whether a keyframe is needed before delta frames can be decoded
6
+ * (after startup or after a gap).
7
+ *
8
+ * Audio is treated as independently decodable (Opus/AAC-LC frames are
9
+ * self-contained) — no keyframe wait needed.
10
+ *
11
+ * ```
12
+ * IDLE ──configure──► NEEDS_KEYFRAME ──keyframe──► DECODING ──gap──► NEEDS_KEYFRAME
13
+ * │
14
+ * END_OF_TRACK
15
+ * │
16
+ * ▼
17
+ * ENDED
18
+ * ```
19
+ *
20
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking.independent)
21
+ * @module
22
+ */
23
+ // ─── State constants ────────────────────────────────────────────────
24
+ /**
25
+ * Decoder states.
26
+ */
27
+ export const DecoderState = {
28
+ /** Not yet configured. */
29
+ IDLE: 'idle',
30
+ /** Configured but waiting for a keyframe. */
31
+ NEEDS_KEYFRAME: 'needs_keyframe',
32
+ /** Actively decoding. */
33
+ DECODING: 'decoding',
34
+ /** Track ended. */
35
+ ENDED: 'ended',
36
+ };
37
+ // ─── DecoderStateMachine ────────────────────────────────────────────
38
+ /**
39
+ * Decoder lifecycle FSM.
40
+ *
41
+ * @see draft-ietf-moq-loc-01 §4.2 (decode order)
42
+ */
43
+ export class DecoderStateMachine {
44
+ _state = DecoderState.IDLE;
45
+ _mediaType;
46
+ lastConfig = null;
47
+ constructor(mediaType) {
48
+ this._mediaType = mediaType;
49
+ }
50
+ get state() {
51
+ return this._state;
52
+ }
53
+ get mediaType() {
54
+ return this._mediaType;
55
+ }
56
+ /**
57
+ * Provide codec configuration.
58
+ *
59
+ * If the config is identical to the last one, skip reconfiguration
60
+ * (stay in current state). This avoids resetting to NEEDS_KEYFRAME
61
+ * on every group boundary when the server sends videoConfig on
62
+ * every group's first object with unchanged codec parameters.
63
+ *
64
+ * Transitions IDLE/DECODING → NEEDS_KEYFRAME (video) or DECODING (audio)
65
+ * only when the config actually changes.
66
+ */
67
+ configure(config) {
68
+ // Skip if config is unchanged
69
+ if (this.lastConfig !== null && this.configEquals(this.lastConfig, config)) {
70
+ return { action: 'skip', reason: 'config unchanged' };
71
+ }
72
+ this.lastConfig = config;
73
+ if (this._mediaType === 'audio') {
74
+ // Audio chunks are always independently decodable — go straight to DECODING
75
+ this._state = DecoderState.DECODING;
76
+ }
77
+ else {
78
+ this._state = DecoderState.NEEDS_KEYFRAME;
79
+ }
80
+ return { action: 'configure', config };
81
+ }
82
+ /** Byte-level config comparison. */
83
+ configEquals(a, b) {
84
+ if (a.length !== b.length)
85
+ return false;
86
+ for (let i = 0; i < a.length; i++) {
87
+ if (a[i] !== b[i])
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ /**
93
+ * Process a video chunk.
94
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking)
95
+ */
96
+ processVideoChunk(chunk, marking) {
97
+ if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {
98
+ return { action: 'skip', reason: `decoder is ${this._state}` };
99
+ }
100
+ if (this._state === DecoderState.NEEDS_KEYFRAME) {
101
+ // Need a confirmed independent frame to start decoding
102
+ const isKeyframe = marking?.independent === true;
103
+ if (!isKeyframe) {
104
+ return { action: 'skip', reason: 'waiting for keyframe' };
105
+ }
106
+ this._state = DecoderState.DECODING;
107
+ return { action: 'decode', chunk };
108
+ }
109
+ // DECODING — accept any frame
110
+ return { action: 'decode', chunk };
111
+ }
112
+ /**
113
+ * Process an audio chunk. Audio codecs (Opus, AAC-LC) produce
114
+ * independently decodable frames — no keyframe gating needed.
115
+ */
116
+ processAudioChunk(chunk) {
117
+ if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {
118
+ return { action: 'skip', reason: `decoder is ${this._state}` };
119
+ }
120
+ // Audio is always independently decodable — always decode
121
+ this._state = DecoderState.DECODING;
122
+ return { action: 'decode', chunk };
123
+ }
124
+ /**
125
+ * Signal a gap — decoder needs to wait for next keyframe (video only).
126
+ * Audio: stays in DECODING since all chunks are independently decodable.
127
+ */
128
+ notifyGap() {
129
+ if (this._mediaType === 'audio') {
130
+ // Audio can resume immediately — all chunks are key
131
+ return { action: 'reset', reason: 'gap detected (audio will resume immediately)' };
132
+ }
133
+ this._state = DecoderState.NEEDS_KEYFRAME;
134
+ return { action: 'reset', reason: 'gap detected' };
135
+ }
136
+ /**
137
+ * Signal track ended.
138
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
139
+ */
140
+ notifyTrackEnded() {
141
+ this._state = DecoderState.ENDED;
142
+ return { action: 'flush' };
143
+ }
144
+ }
145
+ //# sourceMappingURL=decoder-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoder-state.js","sourceRoot":"","sources":["../src/decoder-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Group-level gap detection and skip-forward logic.
3
+ *
4
+ * Tracks the expected group sequence for a track. Detects when groups
5
+ * are missing (server dropped them under congestion via priority-based
6
+ * dropping). Decides whether to wait or skip forward.
7
+ *
8
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 (Object Status)
9
+ * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)
10
+ * @module
11
+ */
12
+ import type { ClockSource } from './types.js';
13
+ /**
14
+ * Gap evaluation actions.
15
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
16
+ */
17
+ export declare const GapAction: {
18
+ /** Continue waiting for the missing group. */
19
+ readonly WAIT: "wait";
20
+ /** Skip forward to the next available group with a keyframe. */
21
+ readonly SKIP_FORWARD: "skip_forward";
22
+ /** Track has ended. */
23
+ readonly TRACK_ENDED: "track_ended";
24
+ };
25
+ export type GapActionValue = (typeof GapAction)[keyof typeof GapAction];
26
+ /**
27
+ * Gap evaluation result.
28
+ */
29
+ export interface GapDecision {
30
+ readonly action: GapActionValue;
31
+ /** If SKIP_FORWARD, the group to skip to. */
32
+ readonly targetGroupId?: bigint;
33
+ }
34
+ /**
35
+ * Detects missing groups and decides when to skip forward.
36
+ *
37
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
38
+ */
39
+ export declare class GapDetector {
40
+ private _gapTimeoutUs;
41
+ private readonly clock;
42
+ /** Set of group IDs explicitly terminated via END_OF_GROUP. */
43
+ private readonly endedGroups;
44
+ /** Whether END_OF_TRACK has been received. */
45
+ private trackEnded;
46
+ /** Earliest observation time for each group. */
47
+ private readonly groupFirstSeenUs;
48
+ constructor(config: {
49
+ gapTimeoutUs: number;
50
+ clock: ClockSource;
51
+ });
52
+ /** Current gap timeout in microseconds. Mutable for adaptive tolerance. */
53
+ get gapTimeoutUs(): number;
54
+ set gapTimeoutUs(value: number);
55
+ /**
56
+ * Notify that an object has been received for a group.
57
+ * Records the observation time for timeout calculations.
58
+ */
59
+ observeGroup(groupId: bigint): void;
60
+ /**
61
+ * Notify that an END_OF_GROUP status was received.
62
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x3
63
+ */
64
+ observeEndOfGroup(groupId: bigint): void;
65
+ /**
66
+ * Notify that an END_OF_TRACK status was received.
67
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x4
68
+ */
69
+ observeEndOfTrack(): void;
70
+ /** Whether the track has ended. */
71
+ get isTrackEnded(): boolean;
72
+ /** Reset all state — used when resuming after pause on a live stream. */
73
+ reset(): void;
74
+ /**
75
+ * Evaluate the current gap state.
76
+ *
77
+ * @param lastConsumedGroupId The last group fully consumed by the decoder
78
+ * @param availableGroupIds Groups currently buffered (sorted ascending)
79
+ * @returns Decision: WAIT, SKIP_FORWARD, or TRACK_ENDED
80
+ */
81
+ evaluate(lastConsumedGroupId: bigint, availableGroupIds: bigint[]): GapDecision;
82
+ }
83
+ //# sourceMappingURL=gap-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gap-detector.d.ts","sourceRoot":"","sources":["../src/gap-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C;;;GAGG;AACH,eAAO,MAAM,SAAS;IAClB,8CAA8C;;IAE9C,gEAAgE;;IAEhE,uBAAuB;;CAEjB,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,6CAA6C;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACnC;AAID;;;;GAIG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IAEpC,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IAEjD,8CAA8C;IAC9C,OAAO,CAAC,UAAU,CAAS;IAE3B,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;gBAElD,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE;IAKhE,2EAA2E;IAC3E,IAAI,YAAY,IAAI,MAAM,CAA+B;IACzD,IAAI,YAAY,CAAC,KAAK,EAAE,MAAM,EAAiC;IAE/D;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAMnC;;;OAGG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIxC;;;OAGG;IACH,iBAAiB,IAAI,IAAI;IAIzB,mCAAmC;IACnC,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,yEAAyE;IACzE,KAAK,IAAI,IAAI;IAMb;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,WAAW;CAiDlF"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Group-level gap detection and skip-forward logic.
3
+ *
4
+ * Tracks the expected group sequence for a track. Detects when groups
5
+ * are missing (server dropped them under congestion via priority-based
6
+ * dropping). Decides whether to wait or skip forward.
7
+ *
8
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 (Object Status)
9
+ * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)
10
+ * @module
11
+ */
12
+ // ─── Action constants ───────────────────────────────────────────────
13
+ /**
14
+ * Gap evaluation actions.
15
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
16
+ */
17
+ export const GapAction = {
18
+ /** Continue waiting for the missing group. */
19
+ WAIT: 'wait',
20
+ /** Skip forward to the next available group with a keyframe. */
21
+ SKIP_FORWARD: 'skip_forward',
22
+ /** Track has ended. */
23
+ TRACK_ENDED: 'track_ended',
24
+ };
25
+ // ─── GapDetector ────────────────────────────────────────────────────
26
+ /**
27
+ * Detects missing groups and decides when to skip forward.
28
+ *
29
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
30
+ */
31
+ export class GapDetector {
32
+ _gapTimeoutUs;
33
+ clock;
34
+ /** Set of group IDs explicitly terminated via END_OF_GROUP. */
35
+ endedGroups = new Set();
36
+ /** Whether END_OF_TRACK has been received. */
37
+ trackEnded = false;
38
+ /** Earliest observation time for each group. */
39
+ groupFirstSeenUs = new Map();
40
+ constructor(config) {
41
+ this._gapTimeoutUs = config.gapTimeoutUs;
42
+ this.clock = config.clock;
43
+ }
44
+ /** Current gap timeout in microseconds. Mutable for adaptive tolerance. */
45
+ get gapTimeoutUs() { return this._gapTimeoutUs; }
46
+ set gapTimeoutUs(value) { this._gapTimeoutUs = value; }
47
+ /**
48
+ * Notify that an object has been received for a group.
49
+ * Records the observation time for timeout calculations.
50
+ */
51
+ observeGroup(groupId) {
52
+ if (!this.groupFirstSeenUs.has(groupId)) {
53
+ this.groupFirstSeenUs.set(groupId, this.clock.now());
54
+ }
55
+ }
56
+ /**
57
+ * Notify that an END_OF_GROUP status was received.
58
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x3
59
+ */
60
+ observeEndOfGroup(groupId) {
61
+ this.endedGroups.add(groupId);
62
+ }
63
+ /**
64
+ * Notify that an END_OF_TRACK status was received.
65
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x4
66
+ */
67
+ observeEndOfTrack() {
68
+ this.trackEnded = true;
69
+ }
70
+ /** Whether the track has ended. */
71
+ get isTrackEnded() {
72
+ return this.trackEnded;
73
+ }
74
+ /** Reset all state — used when resuming after pause on a live stream. */
75
+ reset() {
76
+ this.endedGroups.clear();
77
+ this.trackEnded = false;
78
+ this.groupFirstSeenUs.clear();
79
+ }
80
+ /**
81
+ * Evaluate the current gap state.
82
+ *
83
+ * @param lastConsumedGroupId The last group fully consumed by the decoder
84
+ * @param availableGroupIds Groups currently buffered (sorted ascending)
85
+ * @returns Decision: WAIT, SKIP_FORWARD, or TRACK_ENDED
86
+ */
87
+ evaluate(lastConsumedGroupId, availableGroupIds) {
88
+ // Track ended → always report
89
+ if (this.trackEnded) {
90
+ return { action: GapAction.TRACK_ENDED };
91
+ }
92
+ // Nothing available → wait
93
+ if (availableGroupIds.length === 0) {
94
+ return { action: GapAction.WAIT };
95
+ }
96
+ const nextExpected = lastConsumedGroupId + 1n;
97
+ const sorted = [...availableGroupIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
98
+ const firstAvailable = sorted[0];
99
+ // Contiguous — next expected is available
100
+ if (firstAvailable <= nextExpected) {
101
+ return { action: GapAction.WAIT };
102
+ }
103
+ // Gap exists: firstAvailable > nextExpected
104
+ // Check if all intermediate groups are explicitly ended
105
+ let allEnded = true;
106
+ for (let g = nextExpected; g < firstAvailable; g++) {
107
+ if (!this.endedGroups.has(g)) {
108
+ allEnded = false;
109
+ break;
110
+ }
111
+ }
112
+ if (allEnded) {
113
+ // All intermediate groups are explicitly ended → skip immediately
114
+ return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };
115
+ }
116
+ // Implicit gap — check if we've waited long enough since the first
117
+ // available group was observed (that's when the gap became detectable)
118
+ const now = this.clock.now();
119
+ const firstSeenAt = this.groupFirstSeenUs.get(firstAvailable);
120
+ if (firstSeenAt !== undefined) {
121
+ const elapsed = now - firstSeenAt;
122
+ if (elapsed >= this.gapTimeoutUs) {
123
+ return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };
124
+ }
125
+ }
126
+ return { action: GapAction.WAIT };
127
+ }
128
+ }
129
+ //# sourceMappingURL=gap-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gap-detector.js","sourceRoot":"","sources":["../src/gap-detector.ts"],"names":[],"mappings":"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"}