@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.
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,2788 @@
1
+ // src/jitter-buffer.ts
2
+ function compare(a, b) {
3
+ if (a.groupId < b.groupId) return -1;
4
+ if (a.groupId > b.groupId) return 1;
5
+ if (a.objectId < b.objectId) return -1;
6
+ if (a.objectId > b.objectId) return 1;
7
+ return 0;
8
+ }
9
+ var JitterBuffer = class {
10
+ heap = [];
11
+ maxDepth;
12
+ constructor(maxDepth) {
13
+ this.maxDepth = maxDepth;
14
+ }
15
+ /** Number of objects currently buffered. */
16
+ get size() {
17
+ return this.heap.length;
18
+ }
19
+ /**
20
+ * Insert an object into the buffer.
21
+ * @returns true if accepted, false if buffer is full.
22
+ */
23
+ insert(obj) {
24
+ if (this.heap.length >= this.maxDepth) return false;
25
+ this.heap.push(obj);
26
+ this.bubbleUp(this.heap.length - 1);
27
+ return true;
28
+ }
29
+ /**
30
+ * Peek at the minimum object without removing it.
31
+ */
32
+ peek() {
33
+ return this.heap[0];
34
+ }
35
+ /**
36
+ * Remove and return the minimum object.
37
+ */
38
+ extract() {
39
+ if (this.heap.length === 0) return void 0;
40
+ const min = this.heap[0];
41
+ const last = this.heap.pop();
42
+ if (this.heap.length > 0) {
43
+ this.heap[0] = last;
44
+ this.sinkDown(0);
45
+ }
46
+ return min;
47
+ }
48
+ /**
49
+ * Check if the object at (groupId, objectId) is the minimum in the buffer.
50
+ */
51
+ hasContiguous(groupId, objectId) {
52
+ if (this.heap.length === 0) return false;
53
+ const top = this.heap[0];
54
+ return top.groupId === groupId && top.objectId === objectId;
55
+ }
56
+ /**
57
+ * Remove and return all objects for a given group, in sorted order.
58
+ */
59
+ drainGroup(groupId) {
60
+ const result = [];
61
+ const remaining = [];
62
+ for (const obj of this.heap) {
63
+ if (obj.groupId === groupId) {
64
+ result.push(obj);
65
+ } else {
66
+ remaining.push(obj);
67
+ }
68
+ }
69
+ result.sort(compare);
70
+ this.heap.length = 0;
71
+ for (const obj of remaining) {
72
+ this.heap.push(obj);
73
+ }
74
+ this.rebuild();
75
+ return result;
76
+ }
77
+ /**
78
+ * Remove all objects with groupId less than the given threshold.
79
+ * @returns Number of objects removed.
80
+ */
81
+ discardBefore(groupId) {
82
+ const remaining = [];
83
+ let removed = 0;
84
+ for (const obj of this.heap) {
85
+ if (obj.groupId < groupId) {
86
+ removed++;
87
+ } else {
88
+ remaining.push(obj);
89
+ }
90
+ }
91
+ this.heap.length = 0;
92
+ for (const obj of remaining) {
93
+ this.heap.push(obj);
94
+ }
95
+ this.rebuild();
96
+ return removed;
97
+ }
98
+ /**
99
+ * Evict the least-important object from the buffer.
100
+ *
101
+ * Finds the data object with the highest publisherPriority number
102
+ * (= lowest importance per §7: "lower priority number indicates higher
103
+ * priority"). Tiebreaker: oldest group (lowest groupId).
104
+ *
105
+ * Gap objects and objects with undefined priority are not evictable.
106
+ *
107
+ * @returns The evicted object, or undefined if no candidate exists.
108
+ * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)
109
+ */
110
+ evictLowestImportance() {
111
+ if (this.heap.length === 0) return void 0;
112
+ let worstIdx = -1;
113
+ let worstPriority = -1;
114
+ let worstGroupId = -1n;
115
+ for (let i = 0; i < this.heap.length; i++) {
116
+ const obj = this.heap[i];
117
+ if (obj.kind !== "data" || obj.publisherPriority === void 0) continue;
118
+ const p = obj.publisherPriority;
119
+ if (p > worstPriority || p === worstPriority && obj.groupId < worstGroupId) {
120
+ worstIdx = i;
121
+ worstPriority = p;
122
+ worstGroupId = obj.groupId;
123
+ }
124
+ }
125
+ if (worstIdx === -1) return void 0;
126
+ const evicted = this.heap[worstIdx];
127
+ const last = this.heap.pop();
128
+ if (worstIdx < this.heap.length) {
129
+ this.heap[worstIdx] = last;
130
+ this.rebuild();
131
+ }
132
+ return evicted;
133
+ }
134
+ /**
135
+ * Clear all buffered objects.
136
+ */
137
+ clear() {
138
+ this.heap.length = 0;
139
+ }
140
+ // ─── Heap internals ─────────────────────────────────────────────
141
+ bubbleUp(index) {
142
+ while (index > 0) {
143
+ const parentIdx = index - 1 >> 1;
144
+ if (compare(this.heap[index], this.heap[parentIdx]) >= 0) break;
145
+ this.swap(index, parentIdx);
146
+ index = parentIdx;
147
+ }
148
+ }
149
+ sinkDown(index) {
150
+ const length = this.heap.length;
151
+ while (true) {
152
+ let smallest = index;
153
+ const left = 2 * index + 1;
154
+ const right = 2 * index + 2;
155
+ if (left < length && compare(this.heap[left], this.heap[smallest]) < 0) {
156
+ smallest = left;
157
+ }
158
+ if (right < length && compare(this.heap[right], this.heap[smallest]) < 0) {
159
+ smallest = right;
160
+ }
161
+ if (smallest === index) break;
162
+ this.swap(index, smallest);
163
+ index = smallest;
164
+ }
165
+ }
166
+ swap(i, j) {
167
+ const tmp = this.heap[i];
168
+ this.heap[i] = this.heap[j];
169
+ this.heap[j] = tmp;
170
+ }
171
+ /** Rebuild the heap from an unordered array (O(n)). */
172
+ rebuild() {
173
+ for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
174
+ this.sinkDown(i);
175
+ }
176
+ }
177
+ };
178
+
179
+ // src/gap-detector.ts
180
+ var GapAction = {
181
+ /** Continue waiting for the missing group. */
182
+ WAIT: "wait",
183
+ /** Skip forward to the next available group with a keyframe. */
184
+ SKIP_FORWARD: "skip_forward",
185
+ /** Track has ended. */
186
+ TRACK_ENDED: "track_ended"
187
+ };
188
+ var GapDetector = class {
189
+ _gapTimeoutUs;
190
+ clock;
191
+ /** Set of group IDs explicitly terminated via END_OF_GROUP. */
192
+ endedGroups = /* @__PURE__ */ new Set();
193
+ /** Whether END_OF_TRACK has been received. */
194
+ trackEnded = false;
195
+ /** Earliest observation time for each group. */
196
+ groupFirstSeenUs = /* @__PURE__ */ new Map();
197
+ constructor(config) {
198
+ this._gapTimeoutUs = config.gapTimeoutUs;
199
+ this.clock = config.clock;
200
+ }
201
+ /** Current gap timeout in microseconds. Mutable for adaptive tolerance. */
202
+ get gapTimeoutUs() {
203
+ return this._gapTimeoutUs;
204
+ }
205
+ set gapTimeoutUs(value) {
206
+ this._gapTimeoutUs = value;
207
+ }
208
+ /**
209
+ * Notify that an object has been received for a group.
210
+ * Records the observation time for timeout calculations.
211
+ */
212
+ observeGroup(groupId) {
213
+ if (!this.groupFirstSeenUs.has(groupId)) {
214
+ this.groupFirstSeenUs.set(groupId, this.clock.now());
215
+ }
216
+ }
217
+ /**
218
+ * Notify that an END_OF_GROUP status was received.
219
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x3
220
+ */
221
+ observeEndOfGroup(groupId) {
222
+ this.endedGroups.add(groupId);
223
+ }
224
+ /**
225
+ * Notify that an END_OF_TRACK status was received.
226
+ * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x4
227
+ */
228
+ observeEndOfTrack() {
229
+ this.trackEnded = true;
230
+ }
231
+ /** Whether the track has ended. */
232
+ get isTrackEnded() {
233
+ return this.trackEnded;
234
+ }
235
+ /** Reset all state — used when resuming after pause on a live stream. */
236
+ reset() {
237
+ this.endedGroups.clear();
238
+ this.trackEnded = false;
239
+ this.groupFirstSeenUs.clear();
240
+ }
241
+ /**
242
+ * Evaluate the current gap state.
243
+ *
244
+ * @param lastConsumedGroupId The last group fully consumed by the decoder
245
+ * @param availableGroupIds Groups currently buffered (sorted ascending)
246
+ * @returns Decision: WAIT, SKIP_FORWARD, or TRACK_ENDED
247
+ */
248
+ evaluate(lastConsumedGroupId, availableGroupIds) {
249
+ if (this.trackEnded) {
250
+ return { action: GapAction.TRACK_ENDED };
251
+ }
252
+ if (availableGroupIds.length === 0) {
253
+ return { action: GapAction.WAIT };
254
+ }
255
+ const nextExpected = lastConsumedGroupId + 1n;
256
+ const sorted = [...availableGroupIds].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
257
+ const firstAvailable = sorted[0];
258
+ if (firstAvailable <= nextExpected) {
259
+ return { action: GapAction.WAIT };
260
+ }
261
+ let allEnded = true;
262
+ for (let g = nextExpected; g < firstAvailable; g++) {
263
+ if (!this.endedGroups.has(g)) {
264
+ allEnded = false;
265
+ break;
266
+ }
267
+ }
268
+ if (allEnded) {
269
+ return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };
270
+ }
271
+ const now = this.clock.now();
272
+ const firstSeenAt = this.groupFirstSeenUs.get(firstAvailable);
273
+ if (firstSeenAt !== void 0) {
274
+ const elapsed = now - firstSeenAt;
275
+ if (elapsed >= this.gapTimeoutUs) {
276
+ return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };
277
+ }
278
+ }
279
+ return { action: GapAction.WAIT };
280
+ }
281
+ };
282
+
283
+ // src/decoder-state.ts
284
+ var DecoderState = {
285
+ /** Not yet configured. */
286
+ IDLE: "idle",
287
+ /** Configured but waiting for a keyframe. */
288
+ NEEDS_KEYFRAME: "needs_keyframe",
289
+ /** Actively decoding. */
290
+ DECODING: "decoding",
291
+ /** Track ended. */
292
+ ENDED: "ended"
293
+ };
294
+ var DecoderStateMachine = class {
295
+ _state = DecoderState.IDLE;
296
+ _mediaType;
297
+ lastConfig = null;
298
+ constructor(mediaType) {
299
+ this._mediaType = mediaType;
300
+ }
301
+ get state() {
302
+ return this._state;
303
+ }
304
+ get mediaType() {
305
+ return this._mediaType;
306
+ }
307
+ /**
308
+ * Provide codec configuration.
309
+ *
310
+ * If the config is identical to the last one, skip reconfiguration
311
+ * (stay in current state). This avoids resetting to NEEDS_KEYFRAME
312
+ * on every group boundary when the server sends videoConfig on
313
+ * every group's first object with unchanged codec parameters.
314
+ *
315
+ * Transitions IDLE/DECODING → NEEDS_KEYFRAME (video) or DECODING (audio)
316
+ * only when the config actually changes.
317
+ */
318
+ configure(config) {
319
+ if (this.lastConfig !== null && this.configEquals(this.lastConfig, config)) {
320
+ return { action: "skip", reason: "config unchanged" };
321
+ }
322
+ this.lastConfig = config;
323
+ if (this._mediaType === "audio") {
324
+ this._state = DecoderState.DECODING;
325
+ } else {
326
+ this._state = DecoderState.NEEDS_KEYFRAME;
327
+ }
328
+ return { action: "configure", config };
329
+ }
330
+ /** Byte-level config comparison. */
331
+ configEquals(a, b) {
332
+ if (a.length !== b.length) return false;
333
+ for (let i = 0; i < a.length; i++) {
334
+ if (a[i] !== b[i]) return false;
335
+ }
336
+ return true;
337
+ }
338
+ /**
339
+ * Process a video chunk.
340
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking)
341
+ */
342
+ processVideoChunk(chunk, marking) {
343
+ if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {
344
+ return { action: "skip", reason: `decoder is ${this._state}` };
345
+ }
346
+ if (this._state === DecoderState.NEEDS_KEYFRAME) {
347
+ const isKeyframe = marking?.independent === true;
348
+ if (!isKeyframe) {
349
+ return { action: "skip", reason: "waiting for keyframe" };
350
+ }
351
+ this._state = DecoderState.DECODING;
352
+ return { action: "decode", chunk };
353
+ }
354
+ return { action: "decode", chunk };
355
+ }
356
+ /**
357
+ * Process an audio chunk. Audio codecs (Opus, AAC-LC) produce
358
+ * independently decodable frames — no keyframe gating needed.
359
+ */
360
+ processAudioChunk(chunk) {
361
+ if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {
362
+ return { action: "skip", reason: `decoder is ${this._state}` };
363
+ }
364
+ this._state = DecoderState.DECODING;
365
+ return { action: "decode", chunk };
366
+ }
367
+ /**
368
+ * Signal a gap — decoder needs to wait for next keyframe (video only).
369
+ * Audio: stays in DECODING since all chunks are independently decodable.
370
+ */
371
+ notifyGap() {
372
+ if (this._mediaType === "audio") {
373
+ return { action: "reset", reason: "gap detected (audio will resume immediately)" };
374
+ }
375
+ this._state = DecoderState.NEEDS_KEYFRAME;
376
+ return { action: "reset", reason: "gap detected" };
377
+ }
378
+ /**
379
+ * Signal track ended.
380
+ * @see draft-ietf-moq-transport-16 §10.2.1.1
381
+ */
382
+ notifyTrackEnded() {
383
+ this._state = DecoderState.ENDED;
384
+ return { action: "flush" };
385
+ }
386
+ };
387
+
388
+ // src/sync.ts
389
+ var DEFAULT_DROP_THRESHOLD_US = 5e5;
390
+ var DEFAULT_WALL_CLOCK = { now: () => Date.now() * 1e3 };
391
+ var SyncController = class {
392
+ _driftThresholdUs;
393
+ dropThresholdUs;
394
+ clock;
395
+ /** Local clock time when audio reference was set. */
396
+ localBaselineUs;
397
+ /** CaptureTimestamp of the audio reference sample. */
398
+ captureBaselineUs;
399
+ /** Accumulated drift measurement. */
400
+ _currentDriftUs = 0;
401
+ /**
402
+ * Video join offset — shifts video render times forward during live join.
403
+ *
404
+ * When joining a live stream, video starts from the GOP boundary
405
+ * (which has an older CaptureTimestamp than the current audio).
406
+ * This offset shifts video render times forward so the first frames
407
+ * render in the future instead of the past, preventing startup stutter.
408
+ *
409
+ * Snaps to zero when the video timeline catches up to the audio live edge.
410
+ * Audio render times are unaffected.
411
+ */
412
+ _videoJoinOffsetUs = 0;
413
+ // ── Catch-up state ─────────────────────────────────────────
414
+ wallClock;
415
+ targetLatencyMs;
416
+ maxCatchUpRate;
417
+ catchUpThresholdMs;
418
+ catchUpRecoveryMs;
419
+ _catchUpActive = false;
420
+ _currentRate = 1;
421
+ _lastLatencyUs = 0;
422
+ constructor(config) {
423
+ this._driftThresholdUs = config.driftThresholdUs;
424
+ this.dropThresholdUs = config.dropThresholdUs ?? DEFAULT_DROP_THRESHOLD_US;
425
+ this.clock = config.clock;
426
+ this.wallClock = config.wallClock ?? DEFAULT_WALL_CLOCK;
427
+ this.targetLatencyMs = config.targetLatencyMs;
428
+ this.maxCatchUpRate = config.maxCatchUpRate ?? 1;
429
+ this.catchUpThresholdMs = config.catchUpThresholdMs ?? 500;
430
+ this.catchUpRecoveryMs = config.catchUpRecoveryMs ?? 50;
431
+ }
432
+ /** Whether a sync reference has been established. */
433
+ get hasReference() {
434
+ return this.localBaselineUs !== void 0;
435
+ }
436
+ /** Current drift magnitude in microseconds. */
437
+ get currentDriftUs() {
438
+ return this._currentDriftUs;
439
+ }
440
+ /** Current drift threshold in microseconds. Mutable for adaptive tolerance. */
441
+ get driftThresholdUs() {
442
+ return this._driftThresholdUs;
443
+ }
444
+ set driftThresholdUs(value) {
445
+ this._driftThresholdUs = value;
446
+ }
447
+ /**
448
+ * Whether PRESENTATION SCHEDULE drift exceeds the threshold.
449
+ *
450
+ * Deliberately has NO live-join suppression. The exact queued schedule
451
+ * already contains the join offset, so missing that schedule during a join
452
+ * is a real miss. (The legacy PTS-era metric needed the suppression because
453
+ * the offset itself showed up as drift; see {@link needsResync}.)
454
+ */
455
+ get presentationDriftExceeded() {
456
+ return Math.abs(this._currentDriftUs) >= this._driftThresholdUs;
457
+ }
458
+ /**
459
+ * @deprecated Threshold check for the legacy {@link reportActualRenderTime}
460
+ * metric, retaining its live-join suppression. Use
461
+ * {@link presentationDriftExceeded} with schedule-based measurement.
462
+ */
463
+ get needsResync() {
464
+ if (this._videoJoinOffsetUs > 0) return false;
465
+ return Math.abs(this._currentDriftUs) >= this._driftThresholdUs;
466
+ }
467
+ /** Whether catch-up is currently active. */
468
+ get catchUpActive() {
469
+ return this._catchUpActive;
470
+ }
471
+ /** Current playback rate (1.0 when catch-up inactive). */
472
+ get currentRate() {
473
+ return this._currentRate;
474
+ }
475
+ /**
476
+ * Last measured latency in microseconds.
477
+ * 0 before first measurement.
478
+ * @see draft-ietf-moq-loc-01 §2.3.1.1
479
+ */
480
+ get latencyUs() {
481
+ return this._lastLatencyUs;
482
+ }
483
+ /**
484
+ * Establish sync reference from an audio sample.
485
+ * Maps captureTimestamp to the current local clock time.
486
+ * Resets drift measurement.
487
+ *
488
+ * @param captureTimestampUs CaptureTimestamp in microseconds
489
+ */
490
+ setAudioReference(captureTimestampUs) {
491
+ this.localBaselineUs = this.clock.now();
492
+ this.captureBaselineUs = captureTimestampUs;
493
+ this._currentDriftUs = 0;
494
+ }
495
+ /**
496
+ * Establish sync reference from a video frame (video-only mode).
497
+ * No-op if a reference already exists — audio has priority.
498
+ *
499
+ * @param captureTimestampUs CaptureTimestamp of the first video frame
500
+ */
501
+ setVideoReference(captureTimestampUs) {
502
+ if (this.hasReference) return;
503
+ this.localBaselineUs = this.clock.now();
504
+ this.captureBaselineUs = captureTimestampUs;
505
+ this._currentDriftUs = 0;
506
+ }
507
+ /** Current video join offset in microseconds. 0 = no offset active. */
508
+ get videoJoinOffsetUs() {
509
+ return this._videoJoinOffsetUs;
510
+ }
511
+ /**
512
+ * Apply video join re-anchor for live stream join.
513
+ *
514
+ * Called when the first video keyframe is decoded after a live join.
515
+ * Computes an offset that shifts video render times forward so the
516
+ * first frame renders at `now + cushion` instead of in the past.
517
+ *
518
+ * The offset is a constant added to all video render times. It preserves
519
+ * inter-frame timing (33ms spacing) while shifting the whole timeline.
520
+ * Snaps to zero when video catches up to the live edge.
521
+ *
522
+ * @param firstKeyframeCaptureUs CaptureTimestamp of the first keyframe
523
+ * @param cushionUs Extra future cushion (default: 16ms ≈ 1 rAF tick)
524
+ */
525
+ onVideoJoin(firstKeyframeCaptureUs, cushionUs = 16e3) {
526
+ if (this.localBaselineUs === void 0 || this.captureBaselineUs === void 0) return;
527
+ const naiveRenderTimeUs = this.localBaselineUs + Number(firstKeyframeCaptureUs - this.captureBaselineUs);
528
+ const nowUs = this.clock.now();
529
+ const gapUs = nowUs + cushionUs - naiveRenderTimeUs;
530
+ if (gapUs > 0) {
531
+ this._videoJoinOffsetUs = gapUs;
532
+ }
533
+ }
534
+ /**
535
+ * Compute render timing for a video frame.
536
+ *
537
+ * @param captureTimestampUs CaptureTimestamp in microseconds
538
+ */
539
+ computeVideoRenderTime(captureTimestampUs) {
540
+ const timing = this.computeRenderTime(captureTimestampUs);
541
+ if (timing === null) return null;
542
+ if (this._videoJoinOffsetUs > 0) {
543
+ const adjustedRenderTimeUs = timing.renderTimeUs + this._videoJoinOffsetUs;
544
+ const nowUs = this.clock.now();
545
+ if (timing.renderTimeUs > nowUs) {
546
+ this._videoJoinOffsetUs = 0;
547
+ return timing;
548
+ }
549
+ const offsetUs = adjustedRenderTimeUs - nowUs;
550
+ return {
551
+ renderTimeUs: adjustedRenderTimeUs,
552
+ offsetUs,
553
+ shouldDrop: offsetUs < -this.dropThresholdUs
554
+ };
555
+ }
556
+ return timing;
557
+ }
558
+ /**
559
+ * Compute render timing for an audio sample.
560
+ *
561
+ * @param captureTimestampUs CaptureTimestamp in microseconds
562
+ */
563
+ computeAudioRenderTime(captureTimestampUs) {
564
+ return this.computeRenderTime(captureTimestampUs);
565
+ }
566
+ /**
567
+ * Record PRESENTATION SCHEDULE drift: did the renderer present the frame
568
+ * when it was told to?
569
+ *
570
+ * `scheduledUs` must be the EXACT timestamp stored with the rendered frame.
571
+ * It already accounts for the playout cushion actually applied to that
572
+ * frame, the live-join offset, decode-output recomputation and late-frame
573
+ * handling — none of which may be recomputed or subtracted later, because an
574
+ * adaptive cushion can move between enqueue and presentation.
575
+ *
576
+ * The schedule is REQUIRED here. Optionality lives at
577
+ * `FrameRenderedFeedback`, where the pipeline suppresses the whole
578
+ * measurement/event transaction cleanly: missing evidence is not drift.
579
+ *
580
+ * Actual audio/video skew is measured separately as `sync_skew`.
581
+ */
582
+ reportPresentationTiming(scheduledUs, actualUs) {
583
+ this._currentDriftUs = actualUs - scheduledUs;
584
+ }
585
+ /**
586
+ * @deprecated Compares against the UNCUSHIONED PTS-anchored time, so a
587
+ * stream presenting exactly on its intended schedule reports the whole
588
+ * playout cushion as drift. Retained as a compatibility path only; use
589
+ * {@link reportPresentationTiming}.
590
+ */
591
+ reportActualRenderTime(captureTimestampUs, actualLocalUs) {
592
+ if (this.localBaselineUs === void 0 || this.captureBaselineUs === void 0) return;
593
+ const expectedRenderUs = this.localBaselineUs + Number(captureTimestampUs - this.captureBaselineUs);
594
+ this._currentDriftUs = actualLocalUs - expectedRenderUs;
595
+ }
596
+ // ─── Live Catch-Up ──────────────────────────────────────────────
597
+ /**
598
+ * Measure end-to-end latency from a wall-clock timestamp.
599
+ *
600
+ * Compares the publisher's wall-clock timestamp to the subscriber's
601
+ * wall clock, both in Unix epoch microseconds. A media-time timestamp
602
+ * (LOC-04 with Timescale) has no wall-clock meaning, so latency is
603
+ * unmeasurable and this returns null.
604
+ *
605
+ * @param captureTimestampUs Timestamp in microseconds
606
+ * @param isWallClock False when the timestamp is media time. Undefined means wall clock.
607
+ * @returns Latency in microseconds, or null if unmeasurable
608
+ * @see draft-ietf-moq-loc-01 §2.3.1.1
609
+ * @see draft-ietf-moq-loc-04 §2.3.1.2
610
+ */
611
+ measureLatency(captureTimestampUs, isWallClock) {
612
+ if (captureTimestampUs === 0n || isWallClock === false) return null;
613
+ return this.wallClock.now() - Number(captureTimestampUs);
614
+ }
615
+ /**
616
+ * Evaluate catch-up state based on current latency.
617
+ *
618
+ * Hysteresis: activates when latency > target + threshold,
619
+ * deactivates when latency <= target + recovery.
620
+ *
621
+ * Rate is proportional to overshoot, quantized to 0.05x increments.
622
+ *
623
+ * @param captureTimestampUs CaptureTimestamp in microseconds (Unix epoch)
624
+ * @returns CatchUpState if evaluated, null if catch-up is disabled or no timestamp
625
+ * @see draft-ietf-moq-msf-00 §5.1.16 (targetLatency)
626
+ */
627
+ evaluateCatchUp(captureTimestampUs, isWallClock) {
628
+ const latencyUs = this.measureLatency(captureTimestampUs, isWallClock);
629
+ if (latencyUs === null) return null;
630
+ this._lastLatencyUs = latencyUs;
631
+ if (this.targetLatencyMs === void 0 || this.maxCatchUpRate <= 1) return null;
632
+ const latencyMs = latencyUs / 1e3;
633
+ const targetMs = this.targetLatencyMs;
634
+ if (!this._catchUpActive && latencyMs > targetMs + this.catchUpThresholdMs) {
635
+ this._catchUpActive = true;
636
+ } else if (this._catchUpActive && latencyMs <= targetMs + this.catchUpRecoveryMs) {
637
+ this._catchUpActive = false;
638
+ this._currentRate = 1;
639
+ return { active: false, currentRate: 1, latencyMs, targetMs };
640
+ }
641
+ if (this._catchUpActive) {
642
+ const overshoot = latencyMs - targetMs;
643
+ const ratio = Math.min(Math.max(overshoot / this.catchUpThresholdMs, 0), 1);
644
+ const rawRate = 1 + (this.maxCatchUpRate - 1) * ratio;
645
+ const quantized = Math.round(rawRate * 20) / 20;
646
+ this._currentRate = Math.min(Math.max(quantized, 1), this.maxCatchUpRate);
647
+ }
648
+ return {
649
+ active: this._catchUpActive,
650
+ currentRate: this._currentRate,
651
+ latencyMs,
652
+ targetMs
653
+ };
654
+ }
655
+ /**
656
+ * Reset the sync reference (e.g., after a skip-forward).
657
+ */
658
+ reset() {
659
+ this.localBaselineUs = void 0;
660
+ this.captureBaselineUs = void 0;
661
+ this._currentDriftUs = 0;
662
+ this._videoJoinOffsetUs = 0;
663
+ this._catchUpActive = false;
664
+ this._currentRate = 1;
665
+ }
666
+ // ─── Internal ───────────────────────────────────────────────────
667
+ computeRenderTime(captureTimestampUs) {
668
+ if (this.localBaselineUs === void 0 || this.captureBaselineUs === void 0) {
669
+ return null;
670
+ }
671
+ const now = this.clock.now();
672
+ const renderTimeUs = this.localBaselineUs + Number(captureTimestampUs - this.captureBaselineUs);
673
+ const offsetUs = renderTimeUs - now;
674
+ const shouldDrop = offsetUs < -this.dropThresholdUs;
675
+ return { renderTimeUs, offsetUs, shouldDrop };
676
+ }
677
+ };
678
+
679
+ // src/recovery.ts
680
+ var DefaultRecoveryController = class {
681
+ gapEscalationThreshold;
682
+ maxDecodeErrors;
683
+ windowedThreshold;
684
+ windowedWindowUs;
685
+ stallThresholdMs;
686
+ clock;
687
+ /** Consecutive gap events without a successful decode. */
688
+ consecutiveGaps = 0;
689
+ /**
690
+ * True after reduce_quality fires — gates further escalation until
691
+ * notifySuccess() confirms the quality reduction helped.
692
+ */
693
+ qualityReductionPending = false;
694
+ /** Number of decode errors seen (for escalation). */
695
+ decodeErrorCount = 0;
696
+ /** Timestamps (µs) of recent gap events for windowed escalation. */
697
+ gapTimestamps = [];
698
+ constructor(config) {
699
+ this.gapEscalationThreshold = config?.gapEscalationThreshold ?? 3;
700
+ this.maxDecodeErrors = config?.maxDecodeErrors ?? 2;
701
+ this.windowedThreshold = config?.windowedEscalationThreshold ?? 5;
702
+ this.windowedWindowUs = (config?.windowedEscalationWindowMs ?? 3e4) * 1e3;
703
+ this.stallThresholdMs = config?.stallThresholdMs ?? 300;
704
+ this.clock = null;
705
+ }
706
+ /**
707
+ * Attach a clock source for windowed escalation. Without a clock
708
+ * the windowed path is silently disabled (consecutive-only policy).
709
+ * Kept as a setter rather than a constructor param so existing
710
+ * callers that don't need windowed escalation don't break.
711
+ */
712
+ setClock(clock) {
713
+ this.clock = clock;
714
+ }
715
+ evaluate(trigger) {
716
+ switch (trigger.type) {
717
+ case "gap":
718
+ return this.handleGap();
719
+ case "stall":
720
+ return this.handleStall(trigger.durationMs);
721
+ case "buffer_overflow":
722
+ return this.escalateOrSkip("jitter buffer overflow");
723
+ case "too_far_behind":
724
+ return this.escalateOrSkip("server reported TOO_FAR_BEHIND");
725
+ case "delivery_timeout":
726
+ return { type: "resubscribe" };
727
+ case "decode_error":
728
+ return this.handleDecodeError(trigger.message);
729
+ }
730
+ }
731
+ /**
732
+ * Signal that a frame was successfully decoded/rendered.
733
+ *
734
+ * Resets the consecutive gap counter and re-arms quality escalation.
735
+ * This closes the feedback loop: gaps → reduce_quality → better frames
736
+ * arrive → notifySuccess → ready for next escalation if needed.
737
+ */
738
+ notifySuccess() {
739
+ this.consecutiveGaps = 0;
740
+ this.qualityReductionPending = false;
741
+ }
742
+ /** Reset all state — e.g., after pause→resume. */
743
+ reset() {
744
+ this.consecutiveGaps = 0;
745
+ this.qualityReductionPending = false;
746
+ this.decodeErrorCount = 0;
747
+ this.gapTimestamps.length = 0;
748
+ }
749
+ // ─── Internal ───────────────────────────────────────────────────
750
+ /**
751
+ * Request quality reduction, gated by the pending flag.
752
+ * If a reduce_quality is already pending (waiting for notifySuccess),
753
+ * return skip_forward instead to prevent spam.
754
+ */
755
+ escalateOrSkip(reason) {
756
+ if (this.qualityReductionPending) {
757
+ return { type: "skip_forward" };
758
+ }
759
+ this.qualityReductionPending = true;
760
+ return { type: "reduce_quality", reason };
761
+ }
762
+ handleGap() {
763
+ this.consecutiveGaps++;
764
+ if (this.consecutiveGaps >= this.gapEscalationThreshold) {
765
+ this.consecutiveGaps = 0;
766
+ this.gapTimestamps.length = 0;
767
+ return this.escalateOrSkip("repeated gaps detected");
768
+ }
769
+ if (this.clock) {
770
+ const now = this.clock.now();
771
+ this.gapTimestamps.push(now);
772
+ const cutoff = now - this.windowedWindowUs;
773
+ while (this.gapTimestamps.length > 0 && this.gapTimestamps[0] < cutoff) {
774
+ this.gapTimestamps.shift();
775
+ }
776
+ if (this.gapTimestamps.length >= this.windowedThreshold) {
777
+ this.gapTimestamps.length = 0;
778
+ this.consecutiveGaps = 0;
779
+ return this.escalateOrSkip("repeated stalls within window");
780
+ }
781
+ }
782
+ return { type: "skip_forward" };
783
+ }
784
+ /**
785
+ * Relay signals overload via PUBLISH_DONE/TOO_FAR_BEHIND (§9.13);
786
+ * network bottlenecks produce no server signal, so the player must
787
+ * self-detect via stall rate.
788
+ */
789
+ handleStall(durationMs) {
790
+ if (durationMs < this.stallThresholdMs) {
791
+ return { type: "skip_forward" };
792
+ }
793
+ if (this.clock) {
794
+ const now = this.clock.now();
795
+ this.gapTimestamps.push(now);
796
+ const cutoff = now - this.windowedWindowUs;
797
+ while (this.gapTimestamps.length > 0 && this.gapTimestamps[0] < cutoff) {
798
+ this.gapTimestamps.shift();
799
+ }
800
+ if (this.gapTimestamps.length >= this.windowedThreshold) {
801
+ this.gapTimestamps.length = 0;
802
+ return this.escalateOrSkip("sustained stalls within window");
803
+ }
804
+ }
805
+ return { type: "skip_forward" };
806
+ }
807
+ handleDecodeError(message) {
808
+ this.decodeErrorCount++;
809
+ if (this.decodeErrorCount >= this.maxDecodeErrors) {
810
+ return { type: "terminate", reason: `repeated decode error: ${message}` };
811
+ }
812
+ return { type: "resubscribe" };
813
+ }
814
+ };
815
+
816
+ // src/adaptive-tolerance.ts
817
+ var RecoveryPhase = {
818
+ /** Baseline — no active jitter issues. */
819
+ Normal: "normal",
820
+ /** Gap detected, observing whether transient or persistent. */
821
+ Monitoring: "monitoring",
822
+ /** Persistent gap — waiting with extended timeout before skipping. */
823
+ ExtendedWait: "extended_wait"
824
+ };
825
+ var DEFAULT_TOLERANCE_CONFIG = {
826
+ emaAlpha: 0.125,
827
+ // 1/8 — RFC 6298 gain
828
+ varianceAlpha: 0.25,
829
+ // 1/4 — RFC 6298 gain
830
+ slidingWindowSize: 128,
831
+ // ~4s at 30fps
832
+ jitterMultiplier: 4,
833
+ // μ + 4σ ≈ 99.99% of Gaussian
834
+ minGapTimeoutMs: 50,
835
+ // floor: ~1.5 video frames
836
+ maxGapTimeoutMs: 2e3,
837
+ // ceiling
838
+ minDriftThresholdMs: 80,
839
+ driftThresholdMultiplier: 3,
840
+ stallIncrementMs: 33,
841
+ // 1 video frame interval
842
+ maxStallIncrements: 3,
843
+ // max 3 × 33ms = ~100ms added
844
+ monitorWindowMs: 500,
845
+ // observe before escalating
846
+ monitorCleanFrames: 60,
847
+ // 1s at 60fps tick rate
848
+ monitorCleanDurationMs: 1e3,
849
+ maxExtendedWaitMs: 2e3,
850
+ skipFrequencyThreshold: 3,
851
+ // 3 skips in window → quality reduce
852
+ skipFrequencyWindowMs: 3e4,
853
+ decayMinCleanFrames: 120,
854
+ // 2s at 60fps
855
+ decayMinCleanDurationMs: 2e3,
856
+ decayFactor: 0.9
857
+ // 10% reduction per decay step
858
+ };
859
+ var TICK_NO_SKIP = { shouldSkip: false };
860
+ var TICK_SKIP = { shouldSkip: true };
861
+ var SlidingMax = class {
862
+ buf;
863
+ deq;
864
+ capacity;
865
+ writePos = 0;
866
+ count = 0;
867
+ dHead = 0;
868
+ dTail = 0;
869
+ constructor(capacity) {
870
+ this.capacity = capacity;
871
+ this.buf = new Float64Array(capacity);
872
+ this.deq = new Uint32Array(capacity);
873
+ }
874
+ push(value) {
875
+ const cap = this.capacity;
876
+ const wIdx = this.writePos % cap;
877
+ if (this.count === cap && this.dHead !== this.dTail && this.deq[this.dHead % cap] === wIdx) {
878
+ this.dHead++;
879
+ }
880
+ while (this.dHead !== this.dTail && this.buf[this.deq[(this.dTail - 1) % cap] % cap] <= value) {
881
+ this.dTail--;
882
+ }
883
+ this.deq[this.dTail % cap] = wIdx;
884
+ this.dTail++;
885
+ this.buf[wIdx] = value;
886
+ this.writePos++;
887
+ if (this.count < cap) this.count++;
888
+ }
889
+ get max() {
890
+ if (this.dHead === this.dTail) return 0;
891
+ return this.buf[this.deq[this.dHead % this.capacity] % this.capacity];
892
+ }
893
+ reset() {
894
+ this.writePos = 0;
895
+ this.count = 0;
896
+ this.dHead = 0;
897
+ this.dTail = 0;
898
+ }
899
+ };
900
+ var AdaptiveToleranceController = class {
901
+ cfg;
902
+ // ── Jitter estimation ──
903
+ _jitterEma = 0;
904
+ _jitterVar = 0;
905
+ _lastTransitMs = 0;
906
+ _hasLastTransit = false;
907
+ _slidingMax;
908
+ // ── Phase state ──
909
+ _phase = RecoveryPhase.Normal;
910
+ _phaseEnteredMs = 0;
911
+ _monitorGapCount = 0;
912
+ _stallCount = 0;
913
+ _stallEscalationMs = 0;
914
+ // ── Skip frequency tracking ──
915
+ _skipTimestamps = [];
916
+ // ── Clean streak tracking (for monitoring exit + decay) ──
917
+ _cleanFrameCount = 0;
918
+ _cleanStreakStartMs = 0;
919
+ // ── Quality reduction ──
920
+ _shouldReduceQuality = false;
921
+ // ── Protocol signals ──
922
+ _deliveryTimeoutMs = 0;
923
+ // ── Capture timestamp jitter (for drift threshold) ──
924
+ _lastCaptureUs = 0;
925
+ _hasCaptureRef = false;
926
+ _captureJitterEma = 0;
927
+ constructor(config) {
928
+ this.cfg = config;
929
+ this._slidingMax = new SlidingMax(config.slidingWindowSize);
930
+ }
931
+ // ─── Read-only state ──────────────────────────────────────────
932
+ get phase() {
933
+ return this._phase;
934
+ }
935
+ get jitterEmaMs() {
936
+ return this._jitterEma;
937
+ }
938
+ get jitterVarMs() {
939
+ return this._jitterVar;
940
+ }
941
+ get slidingMaxMs() {
942
+ return this._slidingMax.max;
943
+ }
944
+ get stallCount() {
945
+ return this._stallCount;
946
+ }
947
+ get shouldReduceQuality() {
948
+ return this._shouldReduceQuality;
949
+ }
950
+ /**
951
+ * Auto-calibrated gap timeout in milliseconds.
952
+ *
953
+ * baseline = max(minGap, EMA + K × variance, slidingMax)
954
+ * effective = min(baseline + stallEscalation, maxGap, deliveryTimeout)
955
+ */
956
+ get effectiveGapTimeoutMs() {
957
+ const adaptive = Math.max(
958
+ this.cfg.minGapTimeoutMs,
959
+ this._jitterEma + this.cfg.jitterMultiplier * this._jitterVar,
960
+ this._slidingMax.max
961
+ );
962
+ const withStall = adaptive + this._stallEscalationMs;
963
+ let capped = Math.min(withStall, this.cfg.maxGapTimeoutMs);
964
+ if (this._deliveryTimeoutMs > 0) {
965
+ capped = Math.min(capped, this._deliveryTimeoutMs);
966
+ }
967
+ return capped;
968
+ }
969
+ /**
970
+ * Auto-calibrated drift threshold in milliseconds.
971
+ *
972
+ * Based on observed CaptureTimestamp interarrival jitter.
973
+ */
974
+ get effectiveDriftThresholdMs() {
975
+ if (!this._hasCaptureRef || this._captureJitterEma === 0) {
976
+ return this.cfg.minDriftThresholdMs;
977
+ }
978
+ return Math.max(
979
+ this.cfg.minDriftThresholdMs,
980
+ this._captureJitterEma * this.cfg.driftThresholdMultiplier
981
+ );
982
+ }
983
+ // ─── Events IN ────────────────────────────────────────────────
984
+ /**
985
+ * Notify that a frame arrived.
986
+ *
987
+ * @param nowMs Current time in milliseconds
988
+ * @param captureUs CaptureTimestamp in microseconds
989
+ * @param arrivalMs Arrival time in milliseconds
990
+ */
991
+ onFrameArrived(nowMs, captureUs, arrivalMs) {
992
+ const transitMs = arrivalMs - captureUs * 1e-3;
993
+ if (this._hasLastTransit) {
994
+ const d = transitMs - this._lastTransitMs;
995
+ const absD = d < 0 ? -d : d;
996
+ const emaErr = absD - this._jitterEma;
997
+ this._jitterEma += this.cfg.emaAlpha * emaErr;
998
+ this._jitterVar += this.cfg.varianceAlpha * (absD - this._jitterVar);
999
+ this._slidingMax.push(absD);
1000
+ }
1001
+ this._lastTransitMs = transitMs;
1002
+ this._hasLastTransit = true;
1003
+ if (this._hasCaptureRef) {
1004
+ const captureIntervalUs = captureUs - this._lastCaptureUs;
1005
+ const captureJitterMs = Math.abs(captureIntervalUs) * 1e-3;
1006
+ if (this._captureJitterEma === 0) {
1007
+ this._captureJitterEma = captureJitterMs;
1008
+ } else {
1009
+ this._captureJitterEma += this.cfg.emaAlpha * (captureJitterMs - this._captureJitterEma);
1010
+ }
1011
+ }
1012
+ this._lastCaptureUs = captureUs;
1013
+ this._hasCaptureRef = true;
1014
+ this._cleanFrameCount++;
1015
+ if (this._cleanFrameCount === 1) {
1016
+ this._cleanStreakStartMs = nowMs;
1017
+ }
1018
+ }
1019
+ /**
1020
+ * Notify that a gap was detected.
1021
+ *
1022
+ * @param nowMs Current time in milliseconds
1023
+ * @param groupId The group ID where the gap was detected
1024
+ */
1025
+ onGapDetected(nowMs, _groupId) {
1026
+ this._cleanFrameCount = 0;
1027
+ switch (this._phase) {
1028
+ case RecoveryPhase.Normal:
1029
+ this._phase = RecoveryPhase.Monitoring;
1030
+ this._phaseEnteredMs = nowMs;
1031
+ this._monitorGapCount = 1;
1032
+ break;
1033
+ case RecoveryPhase.Monitoring:
1034
+ this._monitorGapCount++;
1035
+ if (this._monitorGapCount >= 2 && nowMs - this._phaseEnteredMs <= this.cfg.monitorWindowMs) {
1036
+ this._phase = RecoveryPhase.ExtendedWait;
1037
+ this._phaseEnteredMs = nowMs;
1038
+ }
1039
+ break;
1040
+ case RecoveryPhase.ExtendedWait:
1041
+ break;
1042
+ }
1043
+ }
1044
+ /**
1045
+ * Periodic tick — evaluate phase transitions and decay.
1046
+ *
1047
+ * @param nowMs Current time in milliseconds
1048
+ * @returns TickResult indicating whether caller should skip forward
1049
+ */
1050
+ tick(nowMs) {
1051
+ switch (this._phase) {
1052
+ case RecoveryPhase.Monitoring: {
1053
+ if (nowMs - this._phaseEnteredMs > this.cfg.monitorWindowMs) {
1054
+ if (this._cleanFrameCount >= this.cfg.monitorCleanFrames && nowMs - this._cleanStreakStartMs >= this.cfg.monitorCleanDurationMs) {
1055
+ this._phase = RecoveryPhase.Normal;
1056
+ return TICK_NO_SKIP;
1057
+ }
1058
+ }
1059
+ break;
1060
+ }
1061
+ case RecoveryPhase.ExtendedWait: {
1062
+ if (nowMs - this._phaseEnteredMs >= this.cfg.maxExtendedWaitMs) {
1063
+ this._stallCount++;
1064
+ this._stallEscalationMs = Math.min(
1065
+ this._stallCount * this.cfg.stallIncrementMs,
1066
+ this.cfg.maxStallIncrements * this.cfg.stallIncrementMs
1067
+ );
1068
+ this._skipTimestamps.push(nowMs);
1069
+ this._pruneSkipTimestamps(nowMs);
1070
+ if (this._skipTimestamps.length >= this.cfg.skipFrequencyThreshold) {
1071
+ this._shouldReduceQuality = true;
1072
+ }
1073
+ this._phase = RecoveryPhase.Normal;
1074
+ this._cleanFrameCount = 0;
1075
+ return TICK_SKIP;
1076
+ }
1077
+ break;
1078
+ }
1079
+ case RecoveryPhase.Normal: {
1080
+ if (this._stallEscalationMs > 0 && this._cleanFrameCount >= this.cfg.decayMinCleanFrames && this._cleanStreakStartMs > 0 && nowMs - this._cleanStreakStartMs >= this.cfg.decayMinCleanDurationMs) {
1081
+ this._stallEscalationMs *= this.cfg.decayFactor;
1082
+ if (this._stallEscalationMs < 1) {
1083
+ this._stallEscalationMs = 0;
1084
+ this._stallCount = 0;
1085
+ }
1086
+ this._cleanFrameCount = 0;
1087
+ this._cleanStreakStartMs = nowMs;
1088
+ }
1089
+ if (this._shouldReduceQuality) {
1090
+ this._pruneSkipTimestamps(nowMs);
1091
+ if (this._skipTimestamps.length < this.cfg.skipFrequencyThreshold) {
1092
+ this._shouldReduceQuality = false;
1093
+ }
1094
+ }
1095
+ break;
1096
+ }
1097
+ }
1098
+ return TICK_NO_SKIP;
1099
+ }
1100
+ /**
1101
+ * Set DELIVERY_TIMEOUT from protocol negotiation.
1102
+ * Caps the effective gap timeout.
1103
+ *
1104
+ * @see draft-ietf-moq-transport-16 §9.2.2.2
1105
+ */
1106
+ setDeliveryTimeout(timeoutMs) {
1107
+ this._deliveryTimeoutMs = timeoutMs;
1108
+ }
1109
+ /** Reset all state (seek, pause→resume). */
1110
+ reset() {
1111
+ this._jitterEma = 0;
1112
+ this._jitterVar = 0;
1113
+ this._lastTransitMs = 0;
1114
+ this._hasLastTransit = false;
1115
+ this._slidingMax.reset();
1116
+ this._phase = RecoveryPhase.Normal;
1117
+ this._phaseEnteredMs = 0;
1118
+ this._monitorGapCount = 0;
1119
+ this._stallCount = 0;
1120
+ this._stallEscalationMs = 0;
1121
+ this._skipTimestamps = [];
1122
+ this._cleanFrameCount = 0;
1123
+ this._cleanStreakStartMs = 0;
1124
+ this._shouldReduceQuality = false;
1125
+ this._deliveryTimeoutMs = 0;
1126
+ this._lastCaptureUs = 0;
1127
+ this._hasCaptureRef = false;
1128
+ this._captureJitterEma = 0;
1129
+ }
1130
+ // ─── Internal ─────────────────────────────────────────────────
1131
+ _pruneSkipTimestamps(nowMs) {
1132
+ const windowStart = nowMs - this.cfg.skipFrequencyWindowMs;
1133
+ this._skipTimestamps = this._skipTimestamps.filter((t) => t >= windowStart);
1134
+ }
1135
+ };
1136
+
1137
+ // src/render-scheduler.ts
1138
+ var RenderScheduler = class {
1139
+ clock;
1140
+ cushionUs;
1141
+ /** Wall-clock anchor: local time when the first frame was scheduled. */
1142
+ baseTimeUs = null;
1143
+ /** PTS anchor: captureTimestamp of the first frame. */
1144
+ basePtsUs = null;
1145
+ /** Previous frame's arrival time — for burst-to-live detection. */
1146
+ lastArrivalUs = 0;
1147
+ constructor(clock, options) {
1148
+ this.clock = clock;
1149
+ this.cushionUs = options?.cushionUs ?? 16e3;
1150
+ }
1151
+ /**
1152
+ * Compute the wall-clock render time for a decoded frame.
1153
+ *
1154
+ * @param captureTimestampUs CaptureTimestamp from LOC header (microseconds)
1155
+ * @returns Wall-clock time in microseconds when the frame should be painted
1156
+ */
1157
+ schedule(captureTimestampUs) {
1158
+ const nowUs = this.clock.now();
1159
+ const ptsUs = Number(captureTimestampUs);
1160
+ if (this.baseTimeUs === null || this.basePtsUs === null) {
1161
+ this.baseTimeUs = nowUs + this.cushionUs;
1162
+ this.basePtsUs = ptsUs;
1163
+ this.lastArrivalUs = nowUs;
1164
+ return this.baseTimeUs;
1165
+ }
1166
+ const ptsDelta = ptsUs - this.basePtsUs;
1167
+ let renderTimeUs = this.baseTimeUs + ptsDelta;
1168
+ const arrivalDelta = nowUs - this.lastArrivalUs;
1169
+ if (renderTimeUs < nowUs && arrivalDelta > 2e4) {
1170
+ this.baseTimeUs = nowUs + this.cushionUs;
1171
+ this.basePtsUs = ptsUs;
1172
+ renderTimeUs = this.baseTimeUs;
1173
+ }
1174
+ this.lastArrivalUs = nowUs;
1175
+ return renderTimeUs;
1176
+ }
1177
+ /** Reset anchor (seek, pause→resume, gap skip). */
1178
+ reset() {
1179
+ this.baseTimeUs = null;
1180
+ this.basePtsUs = null;
1181
+ this.lastArrivalUs = 0;
1182
+ }
1183
+ };
1184
+
1185
+ // src/startup-buffer.ts
1186
+ function classifyNetwork(handshakeRttMs) {
1187
+ if (handshakeRttMs < 1) {
1188
+ return { minFrames: 2, maxWaitMs: 80 };
1189
+ } else if (handshakeRttMs < 5) {
1190
+ return { minFrames: 3, maxWaitMs: 120 };
1191
+ } else if (handshakeRttMs < 50) {
1192
+ return { minFrames: 8, maxWaitMs: 300 };
1193
+ } else {
1194
+ return { minFrames: 12, maxWaitMs: 500 };
1195
+ }
1196
+ }
1197
+ var StartupBuffer = class {
1198
+ minFrames;
1199
+ maxWaitMs;
1200
+ _frameCount = 0;
1201
+ _firstFrameMs = 0;
1202
+ _lastFrameMs = 0;
1203
+ _draining = false;
1204
+ constructor(params) {
1205
+ this.minFrames = params.minFrames;
1206
+ this.maxWaitMs = params.maxWaitMs;
1207
+ }
1208
+ /**
1209
+ * Record a frame arrival. Call from pushObject().
1210
+ * @param nowMs Current time in milliseconds
1211
+ */
1212
+ recordFrame(nowMs) {
1213
+ this._frameCount++;
1214
+ if (this._frameCount === 1) {
1215
+ this._firstFrameMs = nowMs;
1216
+ }
1217
+ this._lastFrameMs = nowMs;
1218
+ if (!this._draining) {
1219
+ const elapsed = this._lastFrameMs - this._firstFrameMs;
1220
+ if (this._frameCount >= this.minFrames || elapsed >= this.maxWaitMs) {
1221
+ this._draining = true;
1222
+ }
1223
+ }
1224
+ }
1225
+ /** Whether the pipeline should start draining (buffer phase complete). */
1226
+ get shouldDrain() {
1227
+ return this._draining;
1228
+ }
1229
+ /** Number of frames recorded. */
1230
+ get frameCount() {
1231
+ return this._frameCount;
1232
+ }
1233
+ /** Time elapsed since first frame in milliseconds. */
1234
+ get elapsedMs() {
1235
+ if (this._frameCount === 0) return 0;
1236
+ return this._lastFrameMs - this._firstFrameMs;
1237
+ }
1238
+ /** Accelerated EMA alpha for warmup (4x standard 1/16). */
1239
+ get warmupEmaAlpha() {
1240
+ return 0.25;
1241
+ }
1242
+ /** Reset to buffering state (e.g., after seek or pause→resume). */
1243
+ reset() {
1244
+ this._frameCount = 0;
1245
+ this._firstFrameMs = 0;
1246
+ this._lastFrameMs = 0;
1247
+ this._draining = false;
1248
+ }
1249
+ };
1250
+
1251
+ // src/keyframe-validator.ts
1252
+ function isKeyframePayload(codec, payload) {
1253
+ const codecBase = codec.split(".")[0].toLowerCase();
1254
+ switch (codecBase) {
1255
+ case "avc1":
1256
+ case "avc3":
1257
+ return isH264Keyframe(payload);
1258
+ case "hvc1":
1259
+ case "hev1":
1260
+ return isH265Keyframe(payload);
1261
+ case "av01":
1262
+ return isAV1Keyframe(payload);
1263
+ default:
1264
+ return null;
1265
+ }
1266
+ }
1267
+ var MAX_NAL_UNITS_PER_AU = 256;
1268
+ function isH264Keyframe(payload) {
1269
+ if (payload.length < 5) return null;
1270
+ let pos = 0;
1271
+ let iterations = 0;
1272
+ while (pos + 4 < payload.length) {
1273
+ if (++iterations > MAX_NAL_UNITS_PER_AU) return null;
1274
+ const nalLen = payload[pos] << 24 | payload[pos + 1] << 16 | payload[pos + 2] << 8 | payload[pos + 3];
1275
+ pos += 4;
1276
+ if (nalLen <= 0 || pos >= payload.length) break;
1277
+ const nalType = payload[pos] & 31;
1278
+ if (nalType === 5) return true;
1279
+ if (nalType === 7) return true;
1280
+ if (nalType === 6 || nalType === 8 || nalType === 9) {
1281
+ pos += nalLen;
1282
+ continue;
1283
+ }
1284
+ return false;
1285
+ }
1286
+ return null;
1287
+ }
1288
+ function isH265Keyframe(payload) {
1289
+ if (payload.length < 6) return null;
1290
+ let pos = 0;
1291
+ let iterations = 0;
1292
+ while (pos + 4 < payload.length) {
1293
+ if (++iterations > MAX_NAL_UNITS_PER_AU) return null;
1294
+ const nalLen = payload[pos] << 24 | payload[pos + 1] << 16 | payload[pos + 2] << 8 | payload[pos + 3];
1295
+ pos += 4;
1296
+ if (nalLen <= 0 || pos + 1 >= payload.length) break;
1297
+ const nalType = payload[pos] >> 1 & 63;
1298
+ if (nalType === 19 || nalType === 20) return true;
1299
+ if (nalType === 21) return true;
1300
+ if (nalType === 32 || nalType === 33 || nalType === 34) {
1301
+ pos += nalLen;
1302
+ continue;
1303
+ }
1304
+ if (nalType === 35 || nalType === 39 || nalType === 40) {
1305
+ pos += nalLen;
1306
+ continue;
1307
+ }
1308
+ return false;
1309
+ }
1310
+ return null;
1311
+ }
1312
+ function isAV1Keyframe(payload) {
1313
+ if (payload.length < 2) return null;
1314
+ const obuType = payload[0] >> 3 & 15;
1315
+ if (obuType === 1) return true;
1316
+ if (obuType === 2 && payload.length >= 3) {
1317
+ const nextObuType = payload[2] >> 3 & 15;
1318
+ if (nextObuType === 1) return true;
1319
+ }
1320
+ if (obuType === 3 || obuType === 6) return false;
1321
+ return null;
1322
+ }
1323
+
1324
+ // ../transport/dist/primitives/varint.js
1325
+ var MAX_VARINT = 4611686018427387903n;
1326
+ function varint(n) {
1327
+ if (typeof n === "number") {
1328
+ if (!Number.isInteger(n) || n < 0) {
1329
+ throw new RangeError(`Varint value must be a non-negative integer, got ${n}`);
1330
+ }
1331
+ if (n > Number.MAX_SAFE_INTEGER) {
1332
+ throw new RangeError(`Varint number value ${n} exceeds MAX_SAFE_INTEGER; use bigint for values > 2^53-1`);
1333
+ }
1334
+ return BigInt(n);
1335
+ }
1336
+ if (n < 0n || n > MAX_VARINT) {
1337
+ throw new RangeError(`Varint value must be 0 \u2264 n \u2264 ${MAX_VARINT}, got ${n}`);
1338
+ }
1339
+ return n;
1340
+ }
1341
+
1342
+ // ../transport/dist/control/codes-18.js
1343
+ var SetupOption18 = {
1344
+ PATH: 1,
1345
+ AUTHORIZATION_TOKEN: 3,
1346
+ MAX_AUTH_TOKEN_CACHE_SIZE: 4,
1347
+ AUTHORITY: 5,
1348
+ MOQT_IMPLEMENTATION: 7
1349
+ };
1350
+ var SetupOption22 = {
1351
+ ...SetupOption18,
1352
+ MAX_FILTER_RANGES: 6,
1353
+ MAX_REQUEST_UPDATES: 8
1354
+ };
1355
+ var MessageParam18 = {
1356
+ OBJECT_DELIVERY_TIMEOUT: { type: 2, kind: "varint" },
1357
+ AUTHORIZATION_TOKEN: { type: 3, kind: "bytes" },
1358
+ RENDEZVOUS_TIMEOUT: { type: 4, kind: "varint" },
1359
+ SUBGROUP_DELIVERY_TIMEOUT: { type: 6, kind: "varint" },
1360
+ EXPIRES: { type: 8, kind: "varint" },
1361
+ LARGEST_OBJECT: { type: 9, kind: "location" },
1362
+ FILL_TIMEOUT: { type: 10, kind: "varint" },
1363
+ FORWARD: { type: 16, kind: "uint8" },
1364
+ SUBSCRIBER_PRIORITY: { type: 32, kind: "uint8" },
1365
+ SUBSCRIPTION_FILTER: { type: 33, kind: "bytes" },
1366
+ GROUP_ORDER: { type: 34, kind: "uint8" },
1367
+ NEW_GROUP_REQUEST: { type: 50, kind: "varint" },
1368
+ TRACK_NAMESPACE_PREFIX: { type: 52, kind: "namespace" }
1369
+ };
1370
+ var MessageParam22 = {
1371
+ OBJECT_DELIVERY_TIMEOUT: { type: 2, kind: "varint" },
1372
+ AUTHORIZATION_TOKEN: { type: 3, kind: "bytes" },
1373
+ RENDEZVOUS_TIMEOUT: { type: 4, kind: "varint" },
1374
+ SUBGROUP_DELIVERY_TIMEOUT: { type: 6, kind: "varint" },
1375
+ EXPIRES: { type: 8, kind: "varint" },
1376
+ LARGEST_OBJECT: { type: 9, kind: "location" },
1377
+ FILL_TIMEOUT: { type: 10, kind: "varint" },
1378
+ FORWARD: { type: 16, kind: "uint8" },
1379
+ SUBSCRIBER_PRIORITY: { type: 32, kind: "uint8" },
1380
+ LOCATION_FILTER: { type: 33, kind: "locationFilter" },
1381
+ GROUP_ORDER: { type: 34, kind: "uint8" },
1382
+ FILL_PARAMETERS: { type: 35, kind: "bytes" },
1383
+ SUBGROUP_FILTER: { type: 37, kind: "bytes" },
1384
+ OBJECTID_FILTER: { type: 38, kind: "bytes" },
1385
+ PRIORITY_FILTER: { type: 39, kind: "bytes" },
1386
+ OBJECT_PROPERTY_FILTER: { type: 40, kind: "bytes" },
1387
+ TRACK_PROPERTY_FILTER: { type: 41, kind: "bytes" },
1388
+ NEW_GROUP_REQUEST: { type: 50, kind: "varint" },
1389
+ TRACK_NAMESPACE_PREFIX: { type: 52, kind: "namespace" },
1390
+ INCLUDE_PROPERTIES: { type: 53, kind: "uint8" }
1391
+ };
1392
+
1393
+ // ../transport/dist/errors.js
1394
+ var SessionError = {
1395
+ NO_ERROR: varint(0),
1396
+ INTERNAL_ERROR: varint(1),
1397
+ UNAUTHORIZED: varint(2),
1398
+ PROTOCOL_VIOLATION: varint(3),
1399
+ INVALID_REQUEST_ID: varint(4),
1400
+ DUPLICATE_TRACK_ALIAS: varint(5),
1401
+ KEY_VALUE_FORMATTING_ERROR: varint(6),
1402
+ TOO_MANY_REQUESTS: varint(7),
1403
+ INVALID_PATH: varint(8),
1404
+ MALFORMED_PATH: varint(9),
1405
+ GOAWAY_TIMEOUT: varint(16),
1406
+ CONTROL_MESSAGE_TIMEOUT: varint(17),
1407
+ DATA_STREAM_TIMEOUT: varint(18),
1408
+ AUTH_TOKEN_CACHE_OVERFLOW: varint(19),
1409
+ DUPLICATE_AUTH_TOKEN_ALIAS: varint(20),
1410
+ VERSION_NEGOTIATION_FAILED: varint(21),
1411
+ MALFORMED_AUTH_TOKEN: varint(22),
1412
+ UNKNOWN_AUTH_TOKEN_ALIAS: varint(23),
1413
+ EXPIRED_AUTH_TOKEN: varint(24),
1414
+ INVALID_AUTHORITY: varint(25),
1415
+ MALFORMED_AUTHORITY: varint(26),
1416
+ /** draft-22: more than MAX_REQUEST_UPDATES updates outstanding on one request. */
1417
+ TOO_MANY_REQUEST_UPDATES: varint(27)
1418
+ };
1419
+ var RequestError = {
1420
+ INTERNAL_ERROR: varint(0),
1421
+ UNAUTHORIZED: varint(1),
1422
+ TIMEOUT: varint(2),
1423
+ NOT_SUPPORTED: varint(3),
1424
+ MALFORMED_AUTH_TOKEN: varint(4),
1425
+ EXPIRED_AUTH_TOKEN: varint(5),
1426
+ DOES_NOT_EXIST: varint(16),
1427
+ INVALID_RANGE: varint(17),
1428
+ MALFORMED_TRACK: varint(18),
1429
+ DUPLICATE_SUBSCRIPTION: varint(25),
1430
+ UNINTERESTED: varint(32),
1431
+ PREFIX_OVERLAP: varint(48),
1432
+ NAMESPACE_TOO_LARGE: varint(49),
1433
+ INVALID_JOINING_REQUEST_ID: varint(50),
1434
+ /** draft-18 §2.5.1: a Mandatory Track Property the endpoint does not understand. */
1435
+ UNSUPPORTED_EXTENSION: varint(51),
1436
+ /** draft-18 §10.6.2: the request is redirected; the body carries a Redirect. */
1437
+ REDIRECT: varint(52)
1438
+ };
1439
+ var PublishDoneCode = {
1440
+ INTERNAL_ERROR: varint(0),
1441
+ UNAUTHORIZED: varint(1),
1442
+ TRACK_ENDED: varint(2),
1443
+ SUBSCRIPTION_ENDED: varint(3),
1444
+ GOING_AWAY: varint(4),
1445
+ EXPIRED: varint(5),
1446
+ TOO_FAR_BEHIND: varint(6),
1447
+ UPDATE_FAILED: varint(8),
1448
+ MALFORMED_TRACK: varint(18)
1449
+ };
1450
+ var DataStreamError = {
1451
+ INTERNAL_ERROR: varint(0),
1452
+ CANCELLED: varint(1),
1453
+ DELIVERY_TIMEOUT: varint(2),
1454
+ SESSION_CLOSED: varint(3),
1455
+ UNKNOWN_OBJECT_STATUS: varint(4),
1456
+ MALFORMED_TRACK: varint(18)
1457
+ };
1458
+ var RequestError18 = {
1459
+ INTERNAL_ERROR: varint(0),
1460
+ UNAUTHORIZED: varint(1),
1461
+ TIMEOUT: varint(2),
1462
+ NOT_SUPPORTED: varint(3),
1463
+ MALFORMED_AUTH_TOKEN: varint(4),
1464
+ EXPIRED_AUTH_TOKEN: varint(5),
1465
+ GOING_AWAY: varint(6),
1466
+ EXCESSIVE_LOAD: varint(9),
1467
+ DOES_NOT_EXIST: varint(16),
1468
+ INVALID_RANGE: varint(17),
1469
+ MALFORMED_TRACK: varint(18),
1470
+ DUPLICATE_SUBSCRIPTION: varint(25),
1471
+ UNINTERESTED: varint(32),
1472
+ PREFIX_OVERLAP: varint(48),
1473
+ NAMESPACE_TOO_LARGE: varint(49),
1474
+ INVALID_JOINING_REQUEST_ID: varint(50),
1475
+ UNSUPPORTED_EXTENSION: varint(51),
1476
+ REDIRECT: varint(52)
1477
+ };
1478
+ var RequestError22 = {
1479
+ INTERNAL_ERROR: varint(0),
1480
+ UNAUTHORIZED: varint(1),
1481
+ TIMEOUT: varint(2),
1482
+ NOT_SUPPORTED: varint(3),
1483
+ MALFORMED_AUTH_TOKEN: varint(4),
1484
+ EXPIRED_AUTH_TOKEN: varint(5),
1485
+ GOING_AWAY: varint(6),
1486
+ EXCESSIVE_LOAD: varint(9),
1487
+ DOES_NOT_EXIST: varint(16),
1488
+ INVALID_RANGE: varint(17),
1489
+ MALFORMED_TRACK: varint(18),
1490
+ UNINTERESTED: varint(32),
1491
+ PREFIX_OVERLAP: varint(48),
1492
+ NAMESPACE_TOO_LARGE: varint(49),
1493
+ UNSUPPORTED_EXTENSION: varint(51),
1494
+ REDIRECT: varint(52),
1495
+ CONFLICTING_FILTERS: varint(53),
1496
+ INVALID_FILTER: varint(54)
1497
+ };
1498
+ var PublishDoneCode18 = {
1499
+ INTERNAL_ERROR: varint(0),
1500
+ UNAUTHORIZED: varint(1),
1501
+ TRACK_ENDED: varint(2),
1502
+ SUBSCRIPTION_ENDED: varint(3),
1503
+ GOING_AWAY: varint(4),
1504
+ TOO_FAR_BEHIND: varint(5),
1505
+ EXPIRED: varint(6),
1506
+ UPDATE_FAILED: varint(8),
1507
+ EXCESSIVE_LOAD: varint(9),
1508
+ MALFORMED_TRACK: varint(18)
1509
+ };
1510
+ var StreamResetCode18 = {
1511
+ INTERNAL_ERROR: varint(0),
1512
+ CANCELLED: varint(1),
1513
+ DELIVERY_TIMEOUT: varint(2),
1514
+ SESSION_CLOSED: varint(3),
1515
+ GOING_AWAY: varint(4),
1516
+ TOO_FAR_BEHIND: varint(5),
1517
+ UNKNOWN_OBJECT_STATUS: varint(6),
1518
+ EXPIRED_AUTH_TOKEN: varint(7),
1519
+ EXCESSIVE_LOAD: varint(9),
1520
+ MALFORMED_TRACK: varint(18)
1521
+ };
1522
+
1523
+ // ../transport/dist/primitives/bytes.js
1524
+ var SESSION_NAMESPACE_FIELD = new Uint8Array([46, 115, 101, 115, 115, 105, 111, 110]);
1525
+
1526
+ // ../transport/dist/control/message-params-18.js
1527
+ var DEFAULT_MESSAGE_PARAM_REGISTRY = new Map(Object.values(MessageParam18).map((d) => [BigInt(d.type), d.kind]));
1528
+ var DRAFT22_MESSAGE_PARAM_REGISTRY = new Map(Object.values(MessageParam22).map((d) => [BigInt(d.type), d.kind]));
1529
+
1530
+ // ../transport/dist/primitives/reason.js
1531
+ var encoder = new TextEncoder();
1532
+ var decoder = new TextDecoder();
1533
+
1534
+ // ../transport/dist/control/codes.js
1535
+ var MessageType = {
1536
+ REQUEST_UPDATE: varint(2),
1537
+ SUBSCRIBE: varint(3),
1538
+ SUBSCRIBE_OK: varint(4),
1539
+ REQUEST_ERROR: varint(5),
1540
+ PUBLISH_NAMESPACE: varint(6),
1541
+ REQUEST_OK: varint(7),
1542
+ NAMESPACE: varint(8),
1543
+ PUBLISH_NAMESPACE_DONE: varint(9),
1544
+ UNSUBSCRIBE: varint(10),
1545
+ PUBLISH_DONE: varint(11),
1546
+ PUBLISH_NAMESPACE_CANCEL: varint(12),
1547
+ TRACK_STATUS: varint(13),
1548
+ NAMESPACE_DONE: varint(14),
1549
+ GOAWAY: varint(16),
1550
+ SUBSCRIBE_NAMESPACE: varint(17),
1551
+ MAX_REQUEST_ID: varint(21),
1552
+ FETCH: varint(22),
1553
+ FETCH_CANCEL: varint(23),
1554
+ FETCH_OK: varint(24),
1555
+ REQUESTS_BLOCKED: varint(26),
1556
+ PUBLISH: varint(29),
1557
+ PUBLISH_OK: varint(30),
1558
+ CLIENT_SETUP: varint(32),
1559
+ SERVER_SETUP: varint(33)
1560
+ };
1561
+
1562
+ // ../transport/dist/control/parameters.js
1563
+ var SetupParam = {
1564
+ /** @see §9.3.1.2 */
1565
+ PATH: varint(1),
1566
+ /** @see §9.3.1.3 */
1567
+ MAX_REQUEST_ID: varint(2),
1568
+ /** @see §9.3.1.5 */
1569
+ AUTHORIZATION_TOKEN: varint(3),
1570
+ /** @see §9.3.1.4 */
1571
+ MAX_AUTH_TOKEN_CACHE_SIZE: varint(4),
1572
+ /** @see §9.3.1.1 */
1573
+ AUTHORITY: varint(5),
1574
+ /** @see §9.3.1.6 */
1575
+ MOQT_IMPLEMENTATION: varint(7)
1576
+ };
1577
+ var MessageParam = {
1578
+ /** @see §9.2.2.2 */
1579
+ DELIVERY_TIMEOUT: varint(2),
1580
+ /**
1581
+ * draft-18 §10.2.4: OBJECT_DELIVERY_TIMEOUT — same wire type (0x02) as the
1582
+ * draft-14/16 DELIVERY_TIMEOUT; named alias for the draft-18 scope tables.
1583
+ */
1584
+ OBJECT_DELIVERY_TIMEOUT: varint(2),
1585
+ /** @see §9.2.2.1 */
1586
+ AUTHORIZATION_TOKEN: varint(3),
1587
+ /** draft-18 §10.2.6: RENDEZVOUS_TIMEOUT — SUBSCRIBE only. */
1588
+ RENDEZVOUS_TIMEOUT: varint(4),
1589
+ /** draft-18 §10.2.3: SUBGROUP_DELIVERY_TIMEOUT — PUBLISH_OK / SUBSCRIBE / REQUEST_UPDATE. */
1590
+ SUBGROUP_DELIVERY_TIMEOUT: varint(6),
1591
+ /** @see §9.2.2.6 */
1592
+ EXPIRES: varint(8),
1593
+ /** @see §9.2.2.7 */
1594
+ LARGEST_OBJECT: varint(9),
1595
+ /** draft-18 §10.2.5: FILL_TIMEOUT — FETCH only. */
1596
+ FILL_TIMEOUT: varint(10),
1597
+ /** @see §9.2.2.8 */
1598
+ FORWARD: varint(16),
1599
+ /** @see §9.2.2.3 */
1600
+ SUBSCRIBER_PRIORITY: varint(32),
1601
+ /** @see §9.2.2.5 */
1602
+ SUBSCRIPTION_FILTER: varint(33),
1603
+ /** @see §9.2.2.4 */
1604
+ GROUP_ORDER: varint(34),
1605
+ /** @see §9.2.2.9 */
1606
+ NEW_GROUP_REQUEST: varint(50),
1607
+ /**
1608
+ * draft-18 §10.2.14: Track Namespace Prefix. Carried in REQUEST_UPDATE to
1609
+ * change the prefix of a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS. Its value is a
1610
+ * Track Namespace tuple (semantic `Uint8Array[]`), not a KVP scalar — the
1611
+ * draft-14/16 codecs reject it at the KVP boundary.
1612
+ */
1613
+ TRACK_NAMESPACE_PREFIX: varint(52)
1614
+ };
1615
+
1616
+ // ../transport/dist/control/encoder.js
1617
+ var UNIQUE_MESSAGE_PARAMS = /* @__PURE__ */ new Set([
1618
+ MessageParam.DELIVERY_TIMEOUT,
1619
+ MessageParam.EXPIRES,
1620
+ MessageParam.LARGEST_OBJECT,
1621
+ MessageParam.FORWARD,
1622
+ MessageParam.SUBSCRIBER_PRIORITY,
1623
+ MessageParam.SUBSCRIPTION_FILTER,
1624
+ MessageParam.GROUP_ORDER,
1625
+ MessageParam.NEW_GROUP_REQUEST
1626
+ ]);
1627
+ var MESSAGE_PARAM_NAMES = /* @__PURE__ */ new Map([
1628
+ [MessageParam.DELIVERY_TIMEOUT, "DELIVERY_TIMEOUT"],
1629
+ [MessageParam.EXPIRES, "EXPIRES"],
1630
+ [MessageParam.LARGEST_OBJECT, "LARGEST_OBJECT"],
1631
+ [MessageParam.FORWARD, "FORWARD"],
1632
+ [MessageParam.SUBSCRIBER_PRIORITY, "SUBSCRIBER_PRIORITY"],
1633
+ [MessageParam.SUBSCRIPTION_FILTER, "SUBSCRIPTION_FILTER"],
1634
+ [MessageParam.GROUP_ORDER, "GROUP_ORDER"],
1635
+ [MessageParam.NEW_GROUP_REQUEST, "NEW_GROUP_REQUEST"]
1636
+ ]);
1637
+
1638
+ // ../transport/dist/control/decoder.js
1639
+ var codeToType = /* @__PURE__ */ new Map();
1640
+ for (const [name, code] of Object.entries(MessageType)) {
1641
+ codeToType.set(code, name);
1642
+ }
1643
+
1644
+ // ../transport/dist/control/draft14-codec.js
1645
+ var D14 = {
1646
+ // Outbound (subscriber sends)
1647
+ CLIENT_SETUP: 32,
1648
+ SUBSCRIBE: 3,
1649
+ SUBSCRIBE_UPDATE: 2,
1650
+ SUBSCRIBE_NAMESPACE: 17,
1651
+ UNSUBSCRIBE_NAMESPACE: 20,
1652
+ UNSUBSCRIBE: 10,
1653
+ FETCH: 22,
1654
+ FETCH_CANCEL: 23,
1655
+ TRACK_STATUS: 13,
1656
+ PUBLISH_NAMESPACE_OK: 7,
1657
+ PUBLISH_NAMESPACE_ERROR: 8,
1658
+ // Inbound (subscriber receives)
1659
+ SERVER_SETUP: 33,
1660
+ SUBSCRIBE_OK: 4,
1661
+ SUBSCRIBE_ERROR: 5,
1662
+ FETCH_OK: 24,
1663
+ FETCH_ERROR: 25,
1664
+ TRACK_STATUS_OK: 14,
1665
+ TRACK_STATUS_ERROR: 15,
1666
+ SUBSCRIBE_NAMESPACE_OK: 18,
1667
+ SUBSCRIBE_NAMESPACE_ERROR: 19,
1668
+ PUBLISH_NAMESPACE: 6,
1669
+ PUBLISH_NAMESPACE_DONE: 9,
1670
+ PUBLISH_NAMESPACE_CANCEL: 12,
1671
+ PUBLISH_DONE: 11,
1672
+ GOAWAY: 16,
1673
+ MAX_REQUEST_ID: 21,
1674
+ REQUESTS_BLOCKED: 26,
1675
+ // Varint-length messages
1676
+ PUBLISH: 29,
1677
+ PUBLISH_OK: 30,
1678
+ PUBLISH_ERROR: 31
1679
+ };
1680
+ var VARINT_LENGTH_TYPES = /* @__PURE__ */ new Set([D14.PUBLISH, D14.PUBLISH_OK, D14.PUBLISH_ERROR]);
1681
+
1682
+ // ../transport/dist/control/draft18-codec.js
1683
+ var TEXT_ENCODER = new TextEncoder();
1684
+ var TEXT_DECODER = new TextDecoder();
1685
+ var SETUP_SINGLETONS = /* @__PURE__ */ new Set([
1686
+ BigInt(SetupOption18.PATH),
1687
+ BigInt(SetupOption18.AUTHORITY),
1688
+ BigInt(SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE),
1689
+ BigInt(SetupOption18.MOQT_IMPLEMENTATION)
1690
+ ]);
1691
+
1692
+ // ../transport/dist/data/codes.js
1693
+ var ObjectStatus = {
1694
+ /** Normal object (implicit for non-zero length payloads). */
1695
+ NORMAL: varint(0),
1696
+ /**
1697
+ * Object does not exist at any publisher. Valid in draft-14.
1698
+ * @see draft-ietf-moq-transport-14 §10.2.1.1
1699
+ */
1700
+ OBJECT_DOES_NOT_EXIST: varint(1),
1701
+ /** No objects with same Group ID and Object ID >= this exist. */
1702
+ END_OF_GROUP: varint(3),
1703
+ /** No objects at location >= this exist. */
1704
+ END_OF_TRACK: varint(4)
1705
+ };
1706
+
1707
+ // ../transport/dist/data/decoder.js
1708
+ var VALID_STATUS_CODES_V16 = /* @__PURE__ */ new Set([
1709
+ ObjectStatus.NORMAL,
1710
+ ObjectStatus.END_OF_GROUP,
1711
+ ObjectStatus.END_OF_TRACK
1712
+ ]);
1713
+ var VALID_STATUS_CODES_V14 = /* @__PURE__ */ new Set([
1714
+ ObjectStatus.NORMAL,
1715
+ ObjectStatus.OBJECT_DOES_NOT_EXIST,
1716
+ ObjectStatus.END_OF_GROUP,
1717
+ ObjectStatus.END_OF_TRACK
1718
+ ]);
1719
+
1720
+ // ../transport/dist/qlog/trace.js
1721
+ var MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
1722
+
1723
+ // ../transport/dist/control/auth-token.js
1724
+ var AliasType = {
1725
+ /** Retire a previously registered alias. Has Alias, no Type/Value. */
1726
+ DELETE: varint(0),
1727
+ /** Associate an alias with a Token Type + Value. Has Alias, Type, Value. */
1728
+ REGISTER: varint(1),
1729
+ /** Reference a previously registered token by alias. Has Alias, no Type/Value. */
1730
+ USE_ALIAS: varint(2),
1731
+ /** Provide Token Type + Value directly (no caching). No Alias, has Type/Value. */
1732
+ USE_VALUE: varint(3)
1733
+ };
1734
+
1735
+ // ../transport/dist/session/setup.js
1736
+ var KNOWN_SINGLETON_SETUP_OPTIONS_18 = /* @__PURE__ */ new Set([
1737
+ SetupOption18.PATH,
1738
+ // 0x01
1739
+ SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE,
1740
+ // 0x04
1741
+ SetupOption18.AUTHORITY,
1742
+ // 0x05
1743
+ SetupOption18.MOQT_IMPLEMENTATION
1744
+ // 0x07
1745
+ ]);
1746
+
1747
+ // ../transport/dist/session/session.js
1748
+ var KNOWN_MESSAGE_PARAMS = /* @__PURE__ */ new Set([
1749
+ MessageParam.DELIVERY_TIMEOUT,
1750
+ MessageParam.AUTHORIZATION_TOKEN,
1751
+ MessageParam.EXPIRES,
1752
+ MessageParam.LARGEST_OBJECT,
1753
+ MessageParam.FORWARD,
1754
+ MessageParam.SUBSCRIBER_PRIORITY,
1755
+ MessageParam.SUBSCRIPTION_FILTER,
1756
+ MessageParam.GROUP_ORDER,
1757
+ MessageParam.NEW_GROUP_REQUEST,
1758
+ MessageParam.TRACK_NAMESPACE_PREFIX
1759
+ ]);
1760
+ var VALID_PARAMS_FOR_MESSAGE_TYPE = /* @__PURE__ */ new Map([
1761
+ // §9.2.2.2: DELIVERY_TIMEOUT MAY appear in PUBLISH_OK, SUBSCRIBE, REQUEST_UPDATE
1762
+ [MessageParam.DELIVERY_TIMEOUT, /* @__PURE__ */ new Set(["PUBLISH_OK", "SUBSCRIBE", "REQUEST_UPDATE"])],
1763
+ // §9.2.2.1: AUTHORIZATION_TOKEN MAY appear in PUBLISH, SUBSCRIBE, REQUEST_UPDATE,
1764
+ // SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS, FETCH
1765
+ [MessageParam.AUTHORIZATION_TOKEN, /* @__PURE__ */ new Set([
1766
+ "PUBLISH",
1767
+ "SUBSCRIBE",
1768
+ "REQUEST_UPDATE",
1769
+ "SUBSCRIBE_NAMESPACE",
1770
+ "PUBLISH_NAMESPACE",
1771
+ "TRACK_STATUS",
1772
+ "FETCH"
1773
+ ])],
1774
+ // §9.2.2.6: EXPIRES MAY appear in SUBSCRIBE_OK, PUBLISH, PUBLISH_OK
1775
+ [MessageParam.EXPIRES, /* @__PURE__ */ new Set(["SUBSCRIBE_OK", "PUBLISH", "PUBLISH_OK"])],
1776
+ // §9.2.2.7: LARGEST_OBJECT MAY appear in SUBSCRIBE_OK, PUBLISH, REQUEST_OK
1777
+ [MessageParam.LARGEST_OBJECT, /* @__PURE__ */ new Set(["SUBSCRIBE_OK", "PUBLISH", "REQUEST_OK"])],
1778
+ // §9.2.2.8: FORWARD MAY appear in SUBSCRIBE, REQUEST_UPDATE, PUBLISH, PUBLISH_OK, SUBSCRIBE_NAMESPACE
1779
+ [MessageParam.FORWARD, /* @__PURE__ */ new Set(["SUBSCRIBE", "REQUEST_UPDATE", "PUBLISH", "PUBLISH_OK", "SUBSCRIBE_NAMESPACE"])],
1780
+ // §9.2.2.3: SUBSCRIBER_PRIORITY MAY appear in SUBSCRIBE, FETCH, REQUEST_UPDATE, PUBLISH_OK
1781
+ [MessageParam.SUBSCRIBER_PRIORITY, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "PUBLISH_OK"])],
1782
+ // §9.2.2.5: SUBSCRIPTION_FILTER MAY appear in SUBSCRIBE, PUBLISH_OK, REQUEST_UPDATE
1783
+ [MessageParam.SUBSCRIPTION_FILTER, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH_OK", "REQUEST_UPDATE"])],
1784
+ // §9.2.2.4: GROUP_ORDER MAY appear in SUBSCRIBE, PUBLISH_OK, FETCH
1785
+ // Draft-14 also carries it inline on PUBLISH; handle that as a versioned exception.
1786
+ [MessageParam.GROUP_ORDER, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH_OK", "FETCH"])],
1787
+ // §9.2.2.9: NEW_GROUP_REQUEST MAY appear in PUBLISH_OK, SUBSCRIBE, REQUEST_UPDATE
1788
+ [MessageParam.NEW_GROUP_REQUEST, /* @__PURE__ */ new Set(["PUBLISH_OK", "SUBSCRIBE", "REQUEST_UPDATE"])],
1789
+ // draft-18 §10.2.14: TRACK_NAMESPACE_PREFIX MAY appear in REQUEST_UPDATE (to
1790
+ // change the prefix of a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS).
1791
+ [MessageParam.TRACK_NAMESPACE_PREFIX, /* @__PURE__ */ new Set(["REQUEST_UPDATE"])]
1792
+ ]);
1793
+ var KNOWN_MESSAGE_PARAMS_18 = /* @__PURE__ */ new Set([
1794
+ MessageParam.OBJECT_DELIVERY_TIMEOUT,
1795
+ // 0x02
1796
+ MessageParam.AUTHORIZATION_TOKEN,
1797
+ // 0x03
1798
+ MessageParam.RENDEZVOUS_TIMEOUT,
1799
+ // 0x04
1800
+ MessageParam.SUBGROUP_DELIVERY_TIMEOUT,
1801
+ // 0x06
1802
+ MessageParam.EXPIRES,
1803
+ // 0x08
1804
+ MessageParam.LARGEST_OBJECT,
1805
+ // 0x09
1806
+ MessageParam.FILL_TIMEOUT,
1807
+ // 0x0a
1808
+ MessageParam.FORWARD,
1809
+ // 0x10
1810
+ MessageParam.SUBSCRIBER_PRIORITY,
1811
+ // 0x20
1812
+ MessageParam.SUBSCRIPTION_FILTER,
1813
+ // 0x21
1814
+ MessageParam.GROUP_ORDER,
1815
+ // 0x22
1816
+ MessageParam.NEW_GROUP_REQUEST,
1817
+ // 0x32
1818
+ MessageParam.TRACK_NAMESPACE_PREFIX
1819
+ ]);
1820
+ var VALID_PARAMS_FOR_MESSAGE_TYPE_18 = /* @__PURE__ */ new Map([
1821
+ // §10.2.4
1822
+ [MessageParam.OBJECT_DELIVERY_TIMEOUT, /* @__PURE__ */ new Set(["PUBLISH_OK", "SUBSCRIBE", "REQUEST_UPDATE"])],
1823
+ // §10.2.2
1824
+ [MessageParam.AUTHORIZATION_TOKEN, /* @__PURE__ */ new Set([
1825
+ "PUBLISH",
1826
+ "SUBSCRIBE",
1827
+ "REQUEST_UPDATE",
1828
+ "SUBSCRIBE_NAMESPACE",
1829
+ "SUBSCRIBE_TRACKS",
1830
+ "PUBLISH_NAMESPACE",
1831
+ "TRACK_STATUS",
1832
+ "FETCH"
1833
+ ])],
1834
+ // §10.2.6
1835
+ [MessageParam.RENDEZVOUS_TIMEOUT, /* @__PURE__ */ new Set(["SUBSCRIBE"])],
1836
+ // §10.2.3
1837
+ [MessageParam.SUBGROUP_DELIVERY_TIMEOUT, /* @__PURE__ */ new Set(["PUBLISH_OK", "SUBSCRIBE", "REQUEST_UPDATE"])],
1838
+ // §10.2.10
1839
+ [MessageParam.EXPIRES, /* @__PURE__ */ new Set(["SUBSCRIBE_OK", "PUBLISH", "PUBLISH_OK", "REQUEST_UPDATE_OK"])],
1840
+ // §10.2.11
1841
+ [MessageParam.LARGEST_OBJECT, /* @__PURE__ */ new Set(["SUBSCRIBE_OK", "PUBLISH", "REQUEST_UPDATE_OK", "TRACK_STATUS_OK"])],
1842
+ // §10.2.5
1843
+ [MessageParam.FILL_TIMEOUT, /* @__PURE__ */ new Set(["FETCH"])],
1844
+ // §10.2.12
1845
+ [MessageParam.FORWARD, /* @__PURE__ */ new Set(["SUBSCRIBE", "REQUEST_UPDATE", "PUBLISH", "PUBLISH_OK", "SUBSCRIBE_TRACKS"])],
1846
+ // §10.2.7
1847
+ [MessageParam.SUBSCRIBER_PRIORITY, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "PUBLISH_OK"])],
1848
+ // §10.2.9
1849
+ [MessageParam.SUBSCRIPTION_FILTER, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH_OK", "REQUEST_UPDATE"])],
1850
+ // §10.2.8
1851
+ [MessageParam.GROUP_ORDER, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH_OK", "FETCH"])],
1852
+ // §10.2.13
1853
+ [MessageParam.NEW_GROUP_REQUEST, /* @__PURE__ */ new Set(["PUBLISH_OK", "SUBSCRIBE", "REQUEST_UPDATE"])],
1854
+ // §10.2.14 — generic REQUEST_UPDATE scope; the namespace/tracks-target restriction
1855
+ // is enforced contextually in handleIncomingRequestUpdate.
1856
+ [MessageParam.TRACK_NAMESPACE_PREFIX, /* @__PURE__ */ new Set(["REQUEST_UPDATE"])]
1857
+ ]);
1858
+ var FILL_PARAMETERS = 0x23n;
1859
+ var RANGE_FILTER_PARAMS_22 = /* @__PURE__ */ new Set([0x25n, 0x26n, 0x27n, 0x28n, 0x29n]);
1860
+ var KNOWN_MESSAGE_PARAMS_22 = /* @__PURE__ */ new Set([
1861
+ ...KNOWN_MESSAGE_PARAMS_18,
1862
+ FILL_PARAMETERS,
1863
+ ...RANGE_FILTER_PARAMS_22,
1864
+ 0x35n
1865
+ // INCLUDE_PROPERTIES
1866
+ ]);
1867
+ var VALID_PARAMS_FOR_MESSAGE_TYPE_22 = /* @__PURE__ */ new Map([
1868
+ [MessageParam.OBJECT_DELIVERY_TIMEOUT, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1869
+ [MessageParam.AUTHORIZATION_TOKEN, /* @__PURE__ */ new Set([
1870
+ "PUBLISH",
1871
+ "SUBSCRIBE",
1872
+ "REQUEST_UPDATE",
1873
+ "SUBSCRIBE_NAMESPACE",
1874
+ "SUBSCRIBE_TRACKS",
1875
+ "PUBLISH_NAMESPACE",
1876
+ "TRACK_STATUS",
1877
+ "FETCH"
1878
+ ])],
1879
+ [MessageParam.RENDEZVOUS_TIMEOUT, /* @__PURE__ */ new Set(["SUBSCRIBE", "SUBSCRIBE_TRACKS"])],
1880
+ [MessageParam.SUBGROUP_DELIVERY_TIMEOUT, /* @__PURE__ */ new Set(["SUBSCRIBE", "PUBLISH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1881
+ [MessageParam.EXPIRES, /* @__PURE__ */ new Set([
1882
+ "SUBSCRIBE_OK",
1883
+ "PUBLISH",
1884
+ "PUBLISH_OK",
1885
+ "REQUEST_UPDATE_OK",
1886
+ "SUBSCRIBE_NAMESPACE_OK",
1887
+ "SUBSCRIBE_TRACKS_OK",
1888
+ "PUBLISH_NAMESPACE_OK"
1889
+ ])],
1890
+ [MessageParam.LARGEST_OBJECT, /* @__PURE__ */ new Set([
1891
+ "SUBSCRIBE_OK",
1892
+ "PUBLISH",
1893
+ "REQUEST_UPDATE_OK",
1894
+ "TRACK_STATUS_OK",
1895
+ "PUBLISH_STATE_NOTIFY"
1896
+ ])],
1897
+ [MessageParam.FILL_TIMEOUT, /* @__PURE__ */ new Set(["FETCH"])],
1898
+ [MessageParam.FORWARD, /* @__PURE__ */ new Set(["SUBSCRIBE", "REQUEST_UPDATE", "PUBLISH", "SUBSCRIBE_TRACKS", "PUBLISH_STATE_NOTIFY"])],
1899
+ [MessageParam.SUBSCRIBER_PRIORITY, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "PUBLISH", "SUBSCRIBE_TRACKS"])],
1900
+ [MessageParam.SUBSCRIPTION_FILTER, /* @__PURE__ */ new Set([
1901
+ "SUBSCRIBE",
1902
+ "REQUEST_UPDATE",
1903
+ "PUBLISH",
1904
+ "FETCH",
1905
+ "SUBSCRIBE_TRACKS",
1906
+ "PUBLISH_STATE_NOTIFY"
1907
+ ])],
1908
+ [MessageParam.GROUP_ORDER, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "PUBLISH", "SUBSCRIBE_TRACKS"])],
1909
+ [FILL_PARAMETERS, /* @__PURE__ */ new Set(["SUBSCRIBE", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1910
+ [0x25n, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1911
+ [0x26n, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1912
+ [0x27n, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1913
+ [0x28n, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1914
+ [0x29n, /* @__PURE__ */ new Set(["REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1915
+ [MessageParam.NEW_GROUP_REQUEST, /* @__PURE__ */ new Set(["SUBSCRIBE", "REQUEST_UPDATE", "SUBSCRIBE_TRACKS"])],
1916
+ [MessageParam.TRACK_NAMESPACE_PREFIX, /* @__PURE__ */ new Set(["REQUEST_UPDATE"])],
1917
+ [0x35n, /* @__PURE__ */ new Set(["SUBSCRIBE", "FETCH", "TRACK_STATUS", "SUBSCRIBE_TRACKS"])]
1918
+ ]);
1919
+
1920
+ // ../loc/dist/types.js
1921
+ var Loc01PropertyId = {
1922
+ /** Wall-clock capture time in microseconds. @see loc-01 §2.3.1.1 */
1923
+ CAPTURE_TIMESTAMP: 2,
1924
+ /** RFC 9626 video frame marking as an integer. @see loc-01 §2.3.2.2 */
1925
+ VIDEO_FRAME_MARKING: 4,
1926
+ /** RFC 6464 audio level + voice activity. @see loc-01 §2.3.3.1 */
1927
+ AUDIO_LEVEL: 6,
1928
+ /** Video codec configuration extradata. @see loc-01 §2.3.2.1 */
1929
+ VIDEO_CONFIG: 13
1930
+ };
1931
+ var Loc04PropertyId = {
1932
+ /** Timestamp units per second. Absent means microseconds. @see loc-04 §2.3.1.2 */
1933
+ TIMESCALE: 8,
1934
+ /** RFC 9626 video frame marking as 1 to 4 bytes. @see loc-04 §2.3.2.2 */
1935
+ VIDEO_FRAME_MARKING: 9,
1936
+ /** RFC 6464 audio level + voice activity. @see loc-04 §2.3.3.2 */
1937
+ AUDIO_LEVEL: 12,
1938
+ /** Video codec configuration extradata. @see loc-04 §2.3.2.1 */
1939
+ VIDEO_CONFIG: 13,
1940
+ /** Audio codec configuration. @see loc-04 §2.3.3.1 */
1941
+ AUDIO_CONFIG: 15,
1942
+ /** Frame timestamp in Timescale units. @see loc-04 §2.3.1.1 */
1943
+ TIMESTAMP: 16
1944
+ };
1945
+
1946
+ // ../loc/dist/property-map.js
1947
+ var ID01 = {
1948
+ captureTimestamp: BigInt(Loc01PropertyId.CAPTURE_TIMESTAMP),
1949
+ videoFrameMarking: BigInt(Loc01PropertyId.VIDEO_FRAME_MARKING),
1950
+ audioLevel: BigInt(Loc01PropertyId.AUDIO_LEVEL),
1951
+ videoConfig: BigInt(Loc01PropertyId.VIDEO_CONFIG)
1952
+ };
1953
+ var ID04 = {
1954
+ timescale: BigInt(Loc04PropertyId.TIMESCALE),
1955
+ videoFrameMarking: BigInt(Loc04PropertyId.VIDEO_FRAME_MARKING),
1956
+ audioLevel: BigInt(Loc04PropertyId.AUDIO_LEVEL),
1957
+ videoConfig: BigInt(Loc04PropertyId.VIDEO_CONFIG),
1958
+ audioConfig: BigInt(Loc04PropertyId.AUDIO_CONFIG),
1959
+ timestamp: BigInt(Loc04PropertyId.TIMESTAMP)
1960
+ };
1961
+
1962
+ // ../loc/dist/headers.js
1963
+ function sourceTimestamp(headers) {
1964
+ const micros = headers.captureTimestamp;
1965
+ if (micros === void 0)
1966
+ return void 0;
1967
+ const scale = headers.timescale;
1968
+ if (scale !== void 0 && (scale <= 0n || headers.timestampIsWallClock === true))
1969
+ return void 0;
1970
+ if (headers.timestamp !== void 0) {
1971
+ if (scale === void 0 && headers.timestampIsWallClock === false)
1972
+ return void 0;
1973
+ const ticksPerSecond = scale ?? 1000000n;
1974
+ const scaled = headers.timestamp * 1000000n;
1975
+ const truncated = scaled / ticksPerSecond;
1976
+ const rounded = (scaled + (scaled >= 0n ? ticksPerSecond / 2n : -(ticksPerSecond / 2n))) / ticksPerSecond;
1977
+ if (micros !== truncated && micros !== rounded)
1978
+ return void 0;
1979
+ return Object.freeze({ ticks: headers.timestamp, ticksPerSecond, domain: scale === void 0 ? "unix" : "media" });
1980
+ }
1981
+ return Object.freeze({
1982
+ ticks: micros,
1983
+ ticksPerSecond: 1000000n,
1984
+ domain: headers.timestampIsWallClock === true ? "unix" : headers.timestampIsWallClock === false || scale !== void 0 ? "media" : "unknown"
1985
+ });
1986
+ }
1987
+ function toVideoChunkInit(payload, headers) {
1988
+ const isKey = headers.videoFrameMarking?.independent === true;
1989
+ const timestamp = headers.captureTimestamp !== void 0 ? Number(headers.captureTimestamp) : 0;
1990
+ const source = sourceTimestamp(headers);
1991
+ return {
1992
+ type: isKey ? "key" : "delta",
1993
+ timestamp,
1994
+ ...source !== void 0 ? { sourceTimestamp: source } : {},
1995
+ ...headers.duration !== void 0 ? { duration: headers.duration } : {},
1996
+ data: payload
1997
+ };
1998
+ }
1999
+ function toAudioChunkInit(payload, headers) {
2000
+ const timestamp = headers.captureTimestamp !== void 0 ? Number(headers.captureTimestamp) : 0;
2001
+ const source = sourceTimestamp(headers);
2002
+ return {
2003
+ type: "key",
2004
+ timestamp,
2005
+ ...source !== void 0 ? { sourceTimestamp: source } : {},
2006
+ ...headers.duration !== void 0 ? { duration: headers.duration } : {},
2007
+ data: payload
2008
+ };
2009
+ }
2010
+
2011
+ // src/pipeline.ts
2012
+ var PlaybackPipeline = class {
2013
+ mediaType;
2014
+ clock;
2015
+ sync;
2016
+ onCommand;
2017
+ onEvent;
2018
+ recovery;
2019
+ buffer;
2020
+ gapDetector;
2021
+ decoderState;
2022
+ /** Headers stored alongside objects, keyed by "groupId:objectId". */
2023
+ headerMap = /* @__PURE__ */ new Map();
2024
+ /** Group reference counts in the jitter buffer. */
2025
+ bufferedGroupCounts = /* @__PURE__ */ new Map();
2026
+ /** Next expected objectId within the current group for contiguity check. */
2027
+ nextExpectedObjectId = 0n;
2028
+ /** Highest group from which at least one object was consumed. -1n = nothing consumed yet. */
2029
+ lastConsumedGroupId = -1n;
2030
+ /** Groups for which END_OF_GROUP has been observed. */
2031
+ endedGroups = /* @__PURE__ */ new Set();
2032
+ /** Timestamp when we first started waiting for active group completion. */
2033
+ activeGroupWaitStartUs = null;
2034
+ /**
2035
+ * Minimum group ID accepted by pushObject().
2036
+ * Set after SKIP_FORWARD to reject stale objects from pre-skip groups
2037
+ * that arrive late due to async QUIC stream delivery.
2038
+ */
2039
+ minAcceptGroupId = -1n;
2040
+ /** Last group for which keyframe_waiting was emitted (dedup). */
2041
+ lastKeyframeWaitingGroupId = -1n;
2042
+ /**
2043
+ * When true, the first tick syncs to whatever group is available
2044
+ * without gap detection — avoids cascading recovery on initial
2045
+ * live join or after pause→resume on a live stream.
2046
+ *
2047
+ * Starts `true` so the first arriving group is accepted as the
2048
+ * starting point (joining mid-broadcast is not a gap).
2049
+ */
2050
+ resetPending = true;
2051
+ /** True once track_ended has been emitted — prevents re-emission on every tick. */
2052
+ _trackEnded = false;
2053
+ /** Last emitted playback rate — dedup to avoid spamming commands. */
2054
+ _lastEmittedRate = 1;
2055
+ /**
2056
+ * Throttle flag — set by decoder feedback when queue depth is high.
2057
+ * While true, tick() evaluates gaps but does NOT drain the buffer.
2058
+ * Objects still enter the jitter buffer (needed for gap detection).
2059
+ */
2060
+ _throttled = false;
2061
+ /** Adaptive jitter tolerance controller (null if disabled). */
2062
+ adaptiveTolerance;
2063
+ /** Max objects to release per tick (video only). 0 = unlimited. */
2064
+ maxReleasePerTick;
2065
+ /** Max groups before shedding old ones. 0 = unlimited. */
2066
+ maxBacklogGroups;
2067
+ /** Video codec string from catalog (e.g., 'avc1.42c01f'). Used for keyframe validation. */
2068
+ _videoCodec;
2069
+ _videoOnly;
2070
+ constructor(opts) {
2071
+ this.mediaType = opts.mediaType;
2072
+ this.clock = opts.clock;
2073
+ this.sync = opts.sync;
2074
+ this.onCommand = opts.onCommand;
2075
+ this.onEvent = opts.onEvent;
2076
+ this.recovery = opts.recovery;
2077
+ this._videoOnly = opts.videoOnly ?? false;
2078
+ const liveVideo = opts.mediaType === "video" && (opts.isLive ?? true);
2079
+ this.maxReleasePerTick = liveVideo ? opts.config.maxReleasePerTick ?? 5 : 0;
2080
+ this.maxBacklogGroups = liveVideo ? opts.config.maxBacklogGroups ?? 3 : 0;
2081
+ this.buffer = new JitterBuffer(opts.config.maxBufferDepth);
2082
+ this.gapDetector = new GapDetector({
2083
+ gapTimeoutUs: opts.config.gapTimeoutUs,
2084
+ clock: opts.clock
2085
+ });
2086
+ this.decoderState = new DecoderStateMachine(opts.mediaType);
2087
+ this.adaptiveTolerance = opts.config.adaptiveTolerance ? new AdaptiveToleranceController(DEFAULT_TOLERANCE_CONFIG) : null;
2088
+ }
2089
+ /**
2090
+ * Set the video codec string for keyframe payload validation.
2091
+ *
2092
+ * @param codec Codec string from catalog (e.g., 'avc1.42c01f', 'hvc1', 'av01.0.08M.10')
2093
+ * @see draft-ietf-moq-loc-01 §4.2 (decode order)
2094
+ */
2095
+ setCodec(codec) {
2096
+ this._videoCodec = codec;
2097
+ }
2098
+ /**
2099
+ * Current effective gap timeout in microseconds.
2100
+ * When adaptive tolerance is enabled, this is auto-calibrated from jitter.
2101
+ * Otherwise, returns the fixed configured value.
2102
+ */
2103
+ get effectiveGapTimeoutUs() {
2104
+ if (this.adaptiveTolerance) {
2105
+ return this.adaptiveTolerance.effectiveGapTimeoutMs * 1e3;
2106
+ }
2107
+ return this.gapDetector.gapTimeoutUs;
2108
+ }
2109
+ /** Last consumed group ID — for track switch subscription filters. */
2110
+ get currentGroupId() {
2111
+ return this.lastConsumedGroupId;
2112
+ }
2113
+ /** Number of distinct groups currently in the jitter buffer. */
2114
+ get bufferedGroupCount() {
2115
+ return this.bufferedGroupCounts.size;
2116
+ }
2117
+ /** Whether draining is throttled due to decoder backpressure. */
2118
+ get throttled() {
2119
+ return this._throttled;
2120
+ }
2121
+ /**
2122
+ * Handle feedback from browser adapters (decoder queue, errors, drift).
2123
+ *
2124
+ * @see draft-ietf-moq-loc-01 §2.3.1.1 (drift detection)
2125
+ */
2126
+ handleFeedback(fb) {
2127
+ switch (fb.type) {
2128
+ case "queue_pressure":
2129
+ this._throttled = fb.depth >= fb.maxRecommended;
2130
+ break;
2131
+ case "decode_error":
2132
+ this._throttled = false;
2133
+ {
2134
+ const decision = this.decoderState.notifyGap();
2135
+ this.emitFsmDecision(decision);
2136
+ }
2137
+ if (this.recovery) {
2138
+ const action = this.recovery.evaluate({ type: "decode_error", message: fb.message });
2139
+ this.onEvent({ type: "recovery", action });
2140
+ }
2141
+ break;
2142
+ case "frame_rendered":
2143
+ if (fb.scheduledRenderUs !== void 0) {
2144
+ this.sync.reportPresentationTiming(fb.scheduledRenderUs, fb.actualRenderUs);
2145
+ const drift = this.sync.currentDriftUs;
2146
+ if (this.sync.presentationDriftExceeded) {
2147
+ this.onEvent({ type: "sync_drift", driftUs: drift });
2148
+ }
2149
+ }
2150
+ break;
2151
+ case "flush_complete":
2152
+ break;
2153
+ }
2154
+ }
2155
+ /**
2156
+ * Provide codec configuration (from catalog or initial headers).
2157
+ *
2158
+ * This is also triggered automatically when a LOC object carries the
2159
+ * matching config property: Video Config for video, Audio Config for
2160
+ * audio.
2161
+ *
2162
+ * @param config Codec-specific configuration bytes
2163
+ */
2164
+ configure(config) {
2165
+ const decision = this.decoderState.configure(config);
2166
+ if (decision.action === "configure") {
2167
+ this.onCommand({
2168
+ type: "configure",
2169
+ mediaType: this.mediaType,
2170
+ config
2171
+ });
2172
+ }
2173
+ }
2174
+ /**
2175
+ * Push an object into the pipeline.
2176
+ *
2177
+ * Data objects are buffered. Gap objects notify the gap detector.
2178
+ *
2179
+ * @param obj The MoqtObject (data or gap)
2180
+ * @param headers Parsed LocHeaders (only meaningful for data objects)
2181
+ */
2182
+ pushObject(obj, headers) {
2183
+ if (obj.kind === "gap") {
2184
+ if (obj.status === ObjectStatus.END_OF_GROUP) {
2185
+ this.gapDetector.observeEndOfGroup(obj.groupId);
2186
+ this.endedGroups.add(obj.groupId);
2187
+ } else if (obj.status === ObjectStatus.END_OF_TRACK) {
2188
+ this.gapDetector.observeEndOfTrack();
2189
+ }
2190
+ return;
2191
+ }
2192
+ if (obj.groupId < this.minAcceptGroupId) return;
2193
+ if (this.lastConsumedGroupId >= 0n && obj.groupId < this.lastConsumedGroupId) return;
2194
+ if (obj.groupId === this.lastConsumedGroupId && obj.objectId < this.nextExpectedObjectId) return;
2195
+ const key = `${obj.groupId}:${obj.objectId}`;
2196
+ if (this.headerMap.has(key)) return;
2197
+ if (this.buffer.insert(obj)) {
2198
+ this.headerMap.set(key, headers ?? {});
2199
+ const count = this.bufferedGroupCounts.get(obj.groupId) ?? 0;
2200
+ this.bufferedGroupCounts.set(obj.groupId, count + 1);
2201
+ } else if (obj.publisherPriority !== void 0) {
2202
+ if (!this.tryEvictAndInsert(obj, headers) && this.recovery) {
2203
+ this.onEvent({
2204
+ type: "recovery",
2205
+ action: this.recovery.evaluate({ type: "buffer_overflow" })
2206
+ });
2207
+ }
2208
+ } else if (this.recovery) {
2209
+ this.onEvent({
2210
+ type: "recovery",
2211
+ action: this.recovery.evaluate({ type: "buffer_overflow" })
2212
+ });
2213
+ }
2214
+ if (this.adaptiveTolerance && headers?.captureTimestamp !== void 0) {
2215
+ const nowMs = this.clock.now() / 1e3;
2216
+ const captureUs = Number(headers.captureTimestamp);
2217
+ this.adaptiveTolerance.onFrameArrived(nowMs, captureUs, nowMs);
2218
+ }
2219
+ this.gapDetector.observeGroup(obj.groupId);
2220
+ }
2221
+ /**
2222
+ * Reset pipeline state for pause→resume or seek.
2223
+ *
2224
+ * Clears the jitter buffer, gap detector, and decoder FSM so the first
2225
+ * arriving group after resume is accepted cleanly without cascading
2226
+ * gap-recovery escalation from the stale pre-pause position.
2227
+ *
2228
+ * Emits a decoder 'reset' command so the WebCodecs decoder clears its
2229
+ * queue and waits for the next keyframe (video) or resumes immediately
2230
+ * (audio — Opus/AAC-LC frames are independently decodable).
2231
+ *
2232
+ * @param targetGroupId When seeking, set this to the target group ID.
2233
+ * Objects from groups before this ID are rejected — prevents stale
2234
+ * in-flight objects from corrupting the post-seek decoder state.
2235
+ * @see draft-ietf-moq-transport-16 §9.11.1 — "it might still receive
2236
+ * Objects outside the new range if the publisher sent them before the
2237
+ * update was processed."
2238
+ */
2239
+ /**
2240
+ * Reset for a track switch — clears jitter buffer (old-track objects
2241
+ * use the same groupIds as new-track per altGroup alignment), resets
2242
+ * gap detector and group tracking. Does NOT reset decoder state or
2243
+ * emit flush/reset commands — already-decoded frames in the renderer
2244
+ * queue play out naturally for seamless visual transition.
2245
+ *
2246
+ * @param firstGroupId The first group ID from the new track — sets
2247
+ * lastConsumedGroupId to firstGroupId-1 so the gap detector doesn't
2248
+ * see a gap and trigger skip_forward (which would reset sync).
2249
+ */
2250
+ resetForTrackSwitch(firstGroupId) {
2251
+ this.buffer.clear();
2252
+ this.headerMap.clear();
2253
+ this.bufferedGroupCounts.clear();
2254
+ this.lastConsumedGroupId = firstGroupId - 1n;
2255
+ this.endedGroups.add(this.lastConsumedGroupId);
2256
+ this.nextExpectedObjectId = 0n;
2257
+ this.minAcceptGroupId = firstGroupId;
2258
+ this.lastKeyframeWaitingGroupId = -1n;
2259
+ this._throttled = false;
2260
+ this.activeGroupWaitStartUs = null;
2261
+ this.gapDetector.reset();
2262
+ const decision = this.decoderState.notifyGap();
2263
+ this.emitFsmDecision(decision);
2264
+ }
2265
+ reset(targetGroupId) {
2266
+ this.buffer.clear();
2267
+ this.headerMap.clear();
2268
+ this.bufferedGroupCounts.clear();
2269
+ this.lastConsumedGroupId = -1n;
2270
+ this.nextExpectedObjectId = 0n;
2271
+ this.minAcceptGroupId = targetGroupId ?? -1n;
2272
+ this.lastKeyframeWaitingGroupId = -1n;
2273
+ this.resetPending = true;
2274
+ this._throttled = false;
2275
+ this._trackEnded = false;
2276
+ this.endedGroups.clear();
2277
+ this.activeGroupWaitStartUs = null;
2278
+ this.gapDetector.reset();
2279
+ this.adaptiveTolerance?.reset();
2280
+ const decision = this.decoderState.notifyGap();
2281
+ this.emitFsmDecision(decision);
2282
+ }
2283
+ /**
2284
+ * Process buffered objects: evaluate gaps, feed decoder FSM, emit commands.
2285
+ *
2286
+ * Call this on a regular interval (e.g., requestAnimationFrame or timer).
2287
+ */
2288
+ tick() {
2289
+ if (this.adaptiveTolerance) {
2290
+ const nowMs = this.clock.now() / 1e3;
2291
+ this.adaptiveTolerance.tick(nowMs);
2292
+ this.gapDetector.gapTimeoutUs = this.adaptiveTolerance.effectiveGapTimeoutMs * 1e3;
2293
+ this.sync.driftThresholdUs = this.adaptiveTolerance.effectiveDriftThresholdMs * 1e3;
2294
+ }
2295
+ if (this.resetPending && this.buffer.size > 0) {
2296
+ const top = this.buffer.peek();
2297
+ if (top.objectId !== 0n) {
2298
+ this.buffer.extract();
2299
+ this.decrementGroupCount(top.groupId);
2300
+ return;
2301
+ }
2302
+ this.lastConsumedGroupId = top.groupId - 1n;
2303
+ this.endedGroups.add(this.lastConsumedGroupId);
2304
+ this.resetPending = false;
2305
+ }
2306
+ const availableGroupIds = [...this.bufferedGroupCounts.keys()];
2307
+ const gapDecision = this.gapDetector.evaluate(this.lastConsumedGroupId, availableGroupIds);
2308
+ if (gapDecision.action === GapAction.TRACK_ENDED) {
2309
+ if (!this._trackEnded) {
2310
+ this._trackEnded = true;
2311
+ const decision = this.decoderState.notifyTrackEnded();
2312
+ this.emitFsmDecision(decision);
2313
+ this.onEvent({ type: "track_ended" });
2314
+ }
2315
+ return;
2316
+ }
2317
+ if (gapDecision.action === GapAction.SKIP_FORWARD && gapDecision.targetGroupId !== void 0) {
2318
+ const target = gapDecision.targetGroupId;
2319
+ if (this.mediaType === "video" && this.lastConsumedGroupId >= 0n && !this.endedGroups.has(this.lastConsumedGroupId)) {
2320
+ this.onEvent({
2321
+ type: "partial_group_abandoned",
2322
+ fromGroupId: this.lastConsumedGroupId,
2323
+ toGroupId: target,
2324
+ reason: "gap timeout"
2325
+ });
2326
+ }
2327
+ for (const gid of this.endedGroups) {
2328
+ if (gid < target) this.endedGroups.delete(gid);
2329
+ }
2330
+ this.onEvent({
2331
+ type: "skip_forward",
2332
+ fromGroupId: this.lastConsumedGroupId,
2333
+ toGroupId: target
2334
+ });
2335
+ if (this.recovery) {
2336
+ const action = this.recovery.evaluate({ type: "gap", groupId: target });
2337
+ this.onEvent({ type: "recovery", action });
2338
+ }
2339
+ this.minAcceptGroupId = target;
2340
+ this.discardBefore(target);
2341
+ const decision = this.decoderState.notifyGap();
2342
+ this.emitFsmDecision(decision);
2343
+ this.lastConsumedGroupId = target - 1n;
2344
+ this.endedGroups.add(this.lastConsumedGroupId);
2345
+ this.nextExpectedObjectId = 0n;
2346
+ this.activeGroupWaitStartUs = null;
2347
+ }
2348
+ if (this.maxBacklogGroups > 0 && this.bufferedGroupCounts.size > this.maxBacklogGroups) {
2349
+ this.shedBacklog();
2350
+ }
2351
+ if (this._throttled) return;
2352
+ if (this.mediaType === "video" && this.activeGroupWaitStartUs !== null) {
2353
+ const elapsed = this.clock.now() - this.activeGroupWaitStartUs;
2354
+ if (elapsed > this.gapDetector.gapTimeoutUs) {
2355
+ const nextGroupId = this.lastConsumedGroupId + 1n;
2356
+ this.onEvent({
2357
+ type: "partial_group_abandoned",
2358
+ fromGroupId: this.lastConsumedGroupId,
2359
+ toGroupId: nextGroupId,
2360
+ reason: "intra-group timeout"
2361
+ });
2362
+ this.endedGroups.add(this.lastConsumedGroupId);
2363
+ const decision = this.decoderState.notifyGap();
2364
+ this.emitFsmDecision(decision);
2365
+ this.activeGroupWaitStartUs = null;
2366
+ }
2367
+ }
2368
+ let released = 0;
2369
+ const budget = this.maxReleasePerTick;
2370
+ while (this.buffer.size > 0) {
2371
+ if (budget > 0 && released >= budget) break;
2372
+ if (this._throttled) break;
2373
+ const top = this.buffer.peek();
2374
+ if (!top) break;
2375
+ const nextExpectedGroup = this.lastConsumedGroupId + 1n;
2376
+ if (top.groupId > nextExpectedGroup) {
2377
+ break;
2378
+ }
2379
+ if (top.groupId > this.lastConsumedGroupId && this.lastConsumedGroupId >= 0n) {
2380
+ if (this.mediaType === "video" && !this.endedGroups.has(this.lastConsumedGroupId)) {
2381
+ if (this.activeGroupWaitStartUs === null) {
2382
+ this.activeGroupWaitStartUs = this.clock.now();
2383
+ }
2384
+ break;
2385
+ }
2386
+ this.activeGroupWaitStartUs = null;
2387
+ this.endedGroups.delete(this.lastConsumedGroupId);
2388
+ }
2389
+ if (top.groupId > this.lastConsumedGroupId) {
2390
+ this.nextExpectedObjectId = 0n;
2391
+ }
2392
+ if (this.mediaType === "video" && top.objectId > this.nextExpectedObjectId) {
2393
+ const skipGroupId = top.groupId;
2394
+ while (this.buffer.size > 0) {
2395
+ const peek = this.buffer.peek();
2396
+ if (!peek || peek.groupId !== skipGroupId) break;
2397
+ this.buffer.extract();
2398
+ this.decrementGroupCount(skipGroupId);
2399
+ }
2400
+ this.lastConsumedGroupId = skipGroupId;
2401
+ this.nextExpectedObjectId = 0n;
2402
+ const decision = this.decoderState.notifyGap();
2403
+ this.emitFsmDecision(decision);
2404
+ break;
2405
+ }
2406
+ const obj = this.buffer.extract();
2407
+ this.decrementGroupCount(obj.groupId);
2408
+ if (obj.kind === "data") {
2409
+ this.processDataObject(obj);
2410
+ }
2411
+ this.nextExpectedObjectId = obj.objectId + 1n;
2412
+ released++;
2413
+ if (obj.groupId > this.lastConsumedGroupId) {
2414
+ this.lastConsumedGroupId = obj.groupId;
2415
+ }
2416
+ }
2417
+ }
2418
+ // ─── Internal ───────────────────────────────────────────────────
2419
+ /**
2420
+ * Drop oldest whole groups when backlog exceeds maxBacklogGroups.
2421
+ * Keeps the newest maxBacklogGroups groups, discards the rest.
2422
+ * After shedding, notifies decoder that a gap occurred (needs keyframe).
2423
+ */
2424
+ shedBacklog() {
2425
+ const groupIds = [...this.bufferedGroupCounts.keys()].sort(
2426
+ (a, b) => a < b ? -1 : a > b ? 1 : 0
2427
+ );
2428
+ const excess = groupIds.length - this.maxBacklogGroups;
2429
+ if (excess <= 0) return;
2430
+ const groupsToShed = groupIds.slice(0, excess);
2431
+ const cutoff = groupsToShed[groupsToShed.length - 1] + 1n;
2432
+ this.buffer.discardBefore(cutoff);
2433
+ for (const gid of groupsToShed) {
2434
+ this.bufferedGroupCounts.delete(gid);
2435
+ }
2436
+ for (const key of this.headerMap.keys()) {
2437
+ const groupId = BigInt(key.split(":")[0]);
2438
+ if (groupId < cutoff) this.headerMap.delete(key);
2439
+ }
2440
+ const lastShed = groupsToShed[groupsToShed.length - 1];
2441
+ if (lastShed >= this.lastConsumedGroupId) {
2442
+ this.lastConsumedGroupId = lastShed;
2443
+ }
2444
+ this.minAcceptGroupId = lastShed + 1n;
2445
+ const decision = this.decoderState.notifyGap();
2446
+ this.emitFsmDecision(decision);
2447
+ this.onEvent({
2448
+ type: "backlog_shed",
2449
+ droppedGroups: excess,
2450
+ remainingGroups: this.bufferedGroupCounts.size,
2451
+ reason: `backlog exceeded ${this.maxBacklogGroups} groups`
2452
+ });
2453
+ }
2454
+ /**
2455
+ * Whether late encoded VIDEO may be discarded before it reaches the decoder.
2456
+ *
2457
+ * Only explicitly discardable, non-independent frames may be. Everything
2458
+ * else is NOT PROVEN SAFE TO DISCARD and is therefore preserved
2459
+ * conservatively: RFC 9626 §3.1 defines D=1 as the sender asserting the
2460
+ * frame can be removed while the stream stays decodable, so D=0 does not
2461
+ * prove the frame is a reference — it means the receiver has no assurance
2462
+ * that discarding it is safe. Absent marking is treated the same way, and
2463
+ * an independent frame is preserved even if it also claims to be
2464
+ * discardable.
2465
+ *
2466
+ * Lateness of decoded OUTPUT remains the output stage's decision.
2467
+ *
2468
+ * @see draft-ietf-moq-loc-01 §2.3.2.2 (Video Frame Marking)
2469
+ */
2470
+ isPreDecodeDroppable(marking) {
2471
+ if (marking === void 0) return false;
2472
+ if (marking.independent) return false;
2473
+ return marking.discardable === true;
2474
+ }
2475
+ /**
2476
+ * Attempt to evict a lower-importance object to make room.
2477
+ * @returns true if the incoming object was inserted.
2478
+ * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)
2479
+ */
2480
+ tryEvictAndInsert(obj, headers) {
2481
+ const evicted = this.buffer.evictLowestImportance();
2482
+ if (!evicted) return false;
2483
+ if (evicted.publisherPriority > obj.publisherPriority) {
2484
+ this.decrementGroupCount(evicted.groupId);
2485
+ const evictedKey = `${evicted.groupId}:${evicted.objectId}`;
2486
+ this.headerMap.delete(evictedKey);
2487
+ this.buffer.insert(obj);
2488
+ const key = `${obj.groupId}:${obj.objectId}`;
2489
+ this.headerMap.set(key, headers ?? {});
2490
+ const count = this.bufferedGroupCounts.get(obj.groupId) ?? 0;
2491
+ this.bufferedGroupCounts.set(obj.groupId, count + 1);
2492
+ return true;
2493
+ } else {
2494
+ this.buffer.insert(evicted);
2495
+ return false;
2496
+ }
2497
+ }
2498
+ processDataObject(obj) {
2499
+ const key = `${obj.groupId}:${obj.objectId}`;
2500
+ const headers = this.headerMap.get(key) ?? {};
2501
+ this.headerMap.delete(key);
2502
+ const config = this.mediaType === "video" ? headers.videoConfig : headers.audioConfig;
2503
+ if (config) {
2504
+ const configDecision = this.decoderState.configure(config);
2505
+ if (configDecision.action === "configure") {
2506
+ this.onCommand({
2507
+ type: "configure",
2508
+ mediaType: this.mediaType,
2509
+ config
2510
+ });
2511
+ }
2512
+ }
2513
+ if (headers.captureTimestamp !== void 0 && !this.sync.hasReference) {
2514
+ if (this.mediaType === "audio") {
2515
+ this.sync.setAudioReference(headers.captureTimestamp);
2516
+ } else if (this._videoOnly) {
2517
+ this.sync.setVideoReference(headers.captureTimestamp);
2518
+ }
2519
+ }
2520
+ const videoMarking = this.mediaType === "video" ? headers.videoFrameMarking ?? (obj.objectId === 0n ? {
2521
+ independent: true,
2522
+ startOfFrame: true,
2523
+ endOfFrame: true,
2524
+ discardable: false,
2525
+ baseLayerSync: false,
2526
+ temporalId: 0
2527
+ } : void 0) : void 0;
2528
+ let renderTimeUs;
2529
+ if (headers.captureTimestamp !== void 0) {
2530
+ const timing = this.mediaType === "video" ? this.sync.computeVideoRenderTime(headers.captureTimestamp) : this.sync.computeAudioRenderTime(headers.captureTimestamp);
2531
+ if (timing === null) {
2532
+ renderTimeUs = 0;
2533
+ } else if (timing.shouldDrop) {
2534
+ if (timing.offsetUs < -5e6) {
2535
+ renderTimeUs = this.clock.now();
2536
+ } else if (this.mediaType === "video" && !this.isPreDecodeDroppable(videoMarking)) {
2537
+ renderTimeUs = this.clock.now();
2538
+ } else {
2539
+ return;
2540
+ }
2541
+ } else if (timing.offsetUs > 5e6) {
2542
+ renderTimeUs = this.clock.now();
2543
+ } else {
2544
+ renderTimeUs = timing.renderTimeUs;
2545
+ }
2546
+ } else {
2547
+ renderTimeUs = this.clock.now();
2548
+ }
2549
+ if (headers.captureTimestamp !== void 0) {
2550
+ const catchUp = this.sync.evaluateCatchUp(headers.captureTimestamp, headers.timestampIsWallClock);
2551
+ if (catchUp !== null && catchUp.currentRate !== this._lastEmittedRate) {
2552
+ this._lastEmittedRate = catchUp.currentRate;
2553
+ this.onEvent({ type: "catch_up_changed", state: catchUp });
2554
+ this.onCommand({ type: "set_playback_rate", rate: catchUp.currentRate });
2555
+ }
2556
+ }
2557
+ if (this.mediaType === "video") {
2558
+ const marking = videoMarking;
2559
+ if (marking?.independent && this._videoCodec && obj.payload.length > 5) {
2560
+ const isKeyframe = isKeyframePayload(this._videoCodec, obj.payload);
2561
+ if (isKeyframe === false) {
2562
+ this.onEvent({
2563
+ type: "keyframe_validation_failed",
2564
+ groupId: obj.groupId,
2565
+ objectId: obj.objectId,
2566
+ codec: this._videoCodec
2567
+ });
2568
+ }
2569
+ }
2570
+ const headersWithMarking = marking !== void 0 ? { ...headers, videoFrameMarking: marking } : headers;
2571
+ const chunk = toVideoChunkInit(obj.payload, headersWithMarking);
2572
+ const prevState = this.decoderState.state;
2573
+ const decision = this.decoderState.processVideoChunk(chunk, marking);
2574
+ if (prevState === "needs_keyframe" && this.decoderState.state === "decoding" && headers.captureTimestamp !== void 0) {
2575
+ this.sync.onVideoJoin(headers.captureTimestamp);
2576
+ const recomputed = this.sync.computeVideoRenderTime(headers.captureTimestamp);
2577
+ if (recomputed) {
2578
+ renderTimeUs = recomputed.renderTimeUs;
2579
+ }
2580
+ }
2581
+ if (decision.action === "decode") {
2582
+ this.onCommand({
2583
+ type: "decode_video",
2584
+ chunk: decision.chunk,
2585
+ renderTimeUs,
2586
+ ...headers.captureTimestamp !== void 0 ? { captureTimestampUs: headers.captureTimestamp } : {}
2587
+ });
2588
+ } else if (decision.action === "skip") {
2589
+ if (decision.reason === "waiting for keyframe" && obj.groupId !== this.lastKeyframeWaitingGroupId) {
2590
+ this.lastKeyframeWaitingGroupId = obj.groupId;
2591
+ this.onEvent({ type: "keyframe_waiting", groupId: obj.groupId });
2592
+ }
2593
+ }
2594
+ } else {
2595
+ const chunk = toAudioChunkInit(obj.payload, headers);
2596
+ const decision = this.decoderState.processAudioChunk(chunk);
2597
+ if (decision.action === "decode") {
2598
+ this.onCommand({
2599
+ type: "decode_audio",
2600
+ chunk: decision.chunk,
2601
+ renderTimeUs
2602
+ });
2603
+ }
2604
+ }
2605
+ }
2606
+ emitFsmDecision(decision) {
2607
+ if (decision.action === "reset") {
2608
+ this.onCommand({
2609
+ type: "reset",
2610
+ mediaType: this.mediaType,
2611
+ reason: decision.reason ?? "gap"
2612
+ });
2613
+ } else if (decision.action === "flush") {
2614
+ this.onCommand({
2615
+ type: "flush",
2616
+ mediaType: this.mediaType
2617
+ });
2618
+ }
2619
+ }
2620
+ discardBefore(groupId) {
2621
+ this.buffer.discardBefore(groupId);
2622
+ for (const [gid] of this.bufferedGroupCounts) {
2623
+ if (gid < groupId) {
2624
+ this.bufferedGroupCounts.delete(gid);
2625
+ }
2626
+ }
2627
+ for (const [key] of this.headerMap) {
2628
+ const colonIdx = key.indexOf(":");
2629
+ const gid = BigInt(key.slice(0, colonIdx));
2630
+ if (gid < groupId) {
2631
+ this.headerMap.delete(key);
2632
+ }
2633
+ }
2634
+ }
2635
+ decrementGroupCount(groupId) {
2636
+ const count = this.bufferedGroupCounts.get(groupId);
2637
+ if (count !== void 0) {
2638
+ if (count <= 1) {
2639
+ this.bufferedGroupCounts.delete(groupId);
2640
+ } else {
2641
+ this.bufferedGroupCounts.set(groupId, count - 1);
2642
+ }
2643
+ }
2644
+ }
2645
+ };
2646
+
2647
+ // src/bandwidth-estimator.ts
2648
+ var BandwidthEstimator = class {
2649
+ alpha;
2650
+ minSamples;
2651
+ estimateBps = null;
2652
+ lastTimestampUs = null;
2653
+ lastGroupBytes = null;
2654
+ sampleCount = 0;
2655
+ /**
2656
+ * @param alpha EWMA smoothing factor (0-1). Higher = more responsive
2657
+ * to recent samples, lower = smoother. Default 0.3.
2658
+ * @param minSamples Minimum group arrivals before reporting a non-zero
2659
+ * estimate. Early samples include connection latency
2660
+ * and aren't representative. Default 5 (~10s at 2s GOPs).
2661
+ */
2662
+ constructor(alpha = 0.3, minSamples = 5) {
2663
+ this.alpha = alpha;
2664
+ this.minSamples = minSamples;
2665
+ }
2666
+ /**
2667
+ * Record a group arrival. Call once per complete group (GOP).
2668
+ *
2669
+ * @param bytes Total bytes in the group.
2670
+ * @param timestampUs Arrival timestamp in microseconds.
2671
+ */
2672
+ recordGroup(bytes, timestampUs) {
2673
+ if (this.lastTimestampUs !== null && this.lastGroupBytes !== null) {
2674
+ const deltaUs = timestampUs - this.lastTimestampUs;
2675
+ if (deltaUs > 0) {
2676
+ this.sampleCount++;
2677
+ const instantBps = bytes * 8 / (deltaUs / 1e6);
2678
+ if (this.estimateBps === null) {
2679
+ this.estimateBps = instantBps;
2680
+ } else {
2681
+ this.estimateBps = this.alpha * instantBps + (1 - this.alpha) * this.estimateBps;
2682
+ }
2683
+ }
2684
+ }
2685
+ this.lastTimestampUs = timestampUs;
2686
+ this.lastGroupBytes = bytes;
2687
+ }
2688
+ /**
2689
+ * Current smoothed bandwidth estimate in kbps.
2690
+ * Returns 0 if fewer than 2 groups have been recorded.
2691
+ */
2692
+ getEstimateKbps() {
2693
+ if (this.estimateBps === null || this.sampleCount < this.minSamples) return 0;
2694
+ return this.estimateBps / 1e3;
2695
+ }
2696
+ /** Reset all state. */
2697
+ reset() {
2698
+ this.estimateBps = null;
2699
+ this.lastTimestampUs = null;
2700
+ this.lastGroupBytes = null;
2701
+ this.sampleCount = 0;
2702
+ }
2703
+ };
2704
+
2705
+ // src/buffer-based-controller.ts
2706
+ var BufferBasedController = class {
2707
+ ladder;
2708
+ lowThresholdUs;
2709
+ /** Kept for future temporal-buffer mode (playout buffer, not jitter buffer). */
2710
+ highThresholdUs;
2711
+ upshiftMultiple;
2712
+ currentIndex;
2713
+ constructor(config) {
2714
+ this.ladder = config.ladder;
2715
+ this.lowThresholdUs = config.lowThresholdUs;
2716
+ this.highThresholdUs = config.highThresholdUs;
2717
+ this.upshiftMultiple = config.upshiftBandwidthMultiple ?? 2.25;
2718
+ this.currentIndex = config.initialIndex;
2719
+ }
2720
+ /** Current ladder index. */
2721
+ get index() {
2722
+ return this.currentIndex;
2723
+ }
2724
+ /** Current track. */
2725
+ get currentTrack() {
2726
+ return this.ladder[this.currentIndex];
2727
+ }
2728
+ /**
2729
+ * Evaluate buffer depth + bandwidth and return an ABR decision.
2730
+ *
2731
+ * Called periodically (e.g., every tick or every group arrival).
2732
+ */
2733
+ /**
2734
+ * Evaluate buffer depth + bandwidth and return an ABR recommendation.
2735
+ *
2736
+ * Does NOT mutate internal state — the caller must call
2737
+ * {@link commitDownshift} or {@link commitUpshift} after a
2738
+ * confirmed switch. This prevents index drift when async switches
2739
+ * fail or are aborted.
2740
+ */
2741
+ evaluate(signals) {
2742
+ if (signals.bufferDepthUs < this.lowThresholdUs) {
2743
+ if (this.currentIndex < this.ladder.length - 1) {
2744
+ return { action: "downshift", targetIndex: this.currentIndex + 1 };
2745
+ }
2746
+ return { action: "hold" };
2747
+ }
2748
+ if (this.currentIndex > 0 && signals.bufferDepthUs >= this.highThresholdUs) {
2749
+ const higherTrack = this.ladder[this.currentIndex - 1];
2750
+ if (signals.bandwidthEstimateKbps > higherTrack.bitrateKbps * this.upshiftMultiple) {
2751
+ return { action: "upshift", targetIndex: this.currentIndex - 1 };
2752
+ }
2753
+ }
2754
+ return { action: "hold" };
2755
+ }
2756
+ /** Confirm a downshift — advances index to the lower track. */
2757
+ commitDownshift() {
2758
+ if (this.currentIndex < this.ladder.length - 1) this.currentIndex++;
2759
+ }
2760
+ /** Confirm an upshift — moves index to the higher track. */
2761
+ commitUpshift() {
2762
+ if (this.currentIndex > 0) this.currentIndex--;
2763
+ }
2764
+ /** Reset to a specific ladder index (e.g., after manual selection). */
2765
+ setIndex(index) {
2766
+ this.currentIndex = Math.max(0, Math.min(index, this.ladder.length - 1));
2767
+ }
2768
+ };
2769
+ export {
2770
+ AdaptiveToleranceController,
2771
+ BandwidthEstimator,
2772
+ BufferBasedController,
2773
+ DEFAULT_TOLERANCE_CONFIG,
2774
+ DecoderState,
2775
+ DecoderStateMachine,
2776
+ DefaultRecoveryController,
2777
+ GapAction,
2778
+ GapDetector,
2779
+ JitterBuffer,
2780
+ PlaybackPipeline,
2781
+ RecoveryPhase,
2782
+ RenderScheduler,
2783
+ StartupBuffer,
2784
+ SyncController,
2785
+ classifyNetwork,
2786
+ isKeyframePayload
2787
+ };
2788
+ //# sourceMappingURL=index.js.map