@openmoq/playback 0.0.0-stage → 0.6.1
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.
- package/LICENSE.txt +202 -0
- package/README.md +18 -2
- package/dist/adaptive-tolerance.d.ts +128 -0
- package/dist/adaptive-tolerance.d.ts.map +1 -0
- package/dist/adaptive-tolerance.js +339 -0
- package/dist/adaptive-tolerance.js.map +1 -0
- package/dist/bandwidth-estimator.d.ts +49 -0
- package/dist/bandwidth-estimator.d.ts.map +1 -0
- package/dist/bandwidth-estimator.js +77 -0
- package/dist/bandwidth-estimator.js.map +1 -0
- package/dist/browser/index.global.js +2813 -0
- package/dist/browser/index.global.js.map +1 -0
- package/dist/browser/index.js +2788 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/buffer-based-controller.d.ts +87 -0
- package/dist/buffer-based-controller.d.ts.map +1 -0
- package/dist/buffer-based-controller.js +85 -0
- package/dist/buffer-based-controller.js.map +1 -0
- package/dist/decoder-state.d.ts +103 -0
- package/dist/decoder-state.d.ts.map +1 -0
- package/dist/decoder-state.js +145 -0
- package/dist/decoder-state.js.map +1 -0
- package/dist/gap-detector.d.ts +83 -0
- package/dist/gap-detector.d.ts.map +1 -0
- package/dist/gap-detector.js +129 -0
- package/dist/gap-detector.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jitter-buffer.d.ts +73 -0
- package/dist/jitter-buffer.d.ts.map +1 -0
- package/dist/jitter-buffer.js +216 -0
- package/dist/jitter-buffer.js.map +1 -0
- package/dist/keyframe-validator.d.ts +26 -0
- package/dist/keyframe-validator.d.ts.map +1 -0
- package/dist/keyframe-validator.js +170 -0
- package/dist/keyframe-validator.js.map +1 -0
- package/dist/pipeline.d.ts +215 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +852 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/recovery.d.ts +166 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +176 -0
- package/dist/recovery.js.map +1 -0
- package/dist/render-scheduler.d.ts +50 -0
- package/dist/render-scheduler.d.ts.map +1 -0
- package/dist/render-scheduler.js +84 -0
- package/dist/render-scheduler.js.map +1 -0
- package/dist/startup-buffer.d.ts +68 -0
- package/dist/startup-buffer.d.ts.map +1 -0
- package/dist/startup-buffer.js +107 -0
- package/dist/startup-buffer.js.map +1 -0
- package/dist/sync.d.ts +234 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +343 -0
- package/dist/sync.js.map +1 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,uEAAuE;AAEvE;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IACxB,0BAA0B;IAC1B,IAAI,EAAE,MAAM;IACZ,6CAA6C;IAC7C,cAAc,EAAE,gBAAgB;IAChC,yBAAyB;IACzB,QAAQ,EAAE,UAAU;IACpB,mBAAmB;IACnB,KAAK,EAAE,OAAO;CACR,CAAC;AAgBX,uEAAuE;AAEvE;;;;GAIG;AACH,MAAM,OAAO,mBAAmB;IACpB,MAAM,GAAsB,YAAY,CAAC,IAAI,CAAC;IACrC,UAAU,CAAoB;IACvC,UAAU,GAAsB,IAAI,CAAC;IAE7C,YAAY,SAA4B;QACpC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAChC,CAAC;IAED,IAAI,KAAK;QACL,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,IAAI,SAAS;QACT,OAAO,IAAI,CAAC,UAAU,CAAC;IAC3B,CAAC;IAED;;;;;;;;;;OAUG;IACH,SAAS,CAAC,MAAkB;QACxB,8BAA8B;QAC9B,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;YACzE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC;QAEzB,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;YAC9B,4EAA4E;YAC5E,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC;QACxC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,cAAc,CAAC;QAC9C,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAC3C,CAAC;IAED,oCAAoC;IAC5B,YAAY,CAAC,CAAa,EAAE,CAAa;QAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;QACpC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,iBAAiB,CACb,KAAqB,EACrB,OAAsC;QAEtC,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,KAAK,EAAE,CAAC;YAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QACnE,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,cAAc,EAAE,CAAC;YAC9C,uDAAuD;YACvD,MAAM,UAAU,GAAG,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;YACjD,IAAI,CAAC,UAAU,EAAE,CAAC;gBACd,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;YAC9D,CAAC;YACD,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC;YACpC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACvC,CAAC;QAED,8BAA8B;QAC9B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,KAAqB;QACnC,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,CAAC,KAAK,EAAE,CAAC;YAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QACnE,CAAC;QAED,0DAA0D;QAC1D,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC;QACpC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACvC,CAAC;IAED;;;OAGG;IACH,SAAS;QACL,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;YAC9B,oDAAoD;YACpD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,8CAA8C,EAAE,CAAC;QACvF,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,cAAc,CAAC;QAC1C,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACvD,CAAC;IAED;;;OAGG;IACH,gBAAgB;QACZ,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;CACJ"}
|
|
@@ -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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"}
|