@blockcast/mmt-render 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +207 -0
  2. package/dist/audio/audio-buffer-plan.d.ts +27 -0
  3. package/dist/audio/audio-buffer-plan.d.ts.map +1 -0
  4. package/dist/audio/audio-render-pipeline.d.ts +83 -0
  5. package/dist/audio/audio-render-pipeline.d.ts.map +1 -0
  6. package/dist/audio/audio-render-pipeline.js +372 -0
  7. package/dist/audio/audio-render-pipeline.js.map +7 -0
  8. package/dist/audio/audio-ring-buffer.d.ts +55 -0
  9. package/dist/audio/audio-ring-buffer.d.ts.map +1 -0
  10. package/dist/audio/audio-ring-buffer.js +255 -0
  11. package/dist/audio/audio-ring-buffer.js.map +7 -0
  12. package/dist/audio/index.d.ts +9 -0
  13. package/dist/audio/index.d.ts.map +1 -0
  14. package/dist/audio/index.js +640 -0
  15. package/dist/audio/index.js.map +7 -0
  16. package/dist/audio/render-messages.d.ts +94 -0
  17. package/dist/audio/render-messages.d.ts.map +1 -0
  18. package/dist/audio/render-messages.js +1 -0
  19. package/dist/audio/render-messages.js.map +7 -0
  20. package/dist/audio/render-worklet.d.ts +13 -0
  21. package/dist/audio/render-worklet.d.ts.map +1 -0
  22. package/dist/audio/worklet-url.d.ts +25 -0
  23. package/dist/audio/worklet-url.d.ts.map +1 -0
  24. package/dist/audio/worklet-url.js +11 -0
  25. package/dist/audio/worklet-url.js.map +7 -0
  26. package/dist/index.d.ts +13 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +7 -0
  30. package/dist/pacing/au-duration.d.ts +18 -0
  31. package/dist/pacing/au-duration.d.ts.map +1 -0
  32. package/dist/pacing/au-duration.js +23 -0
  33. package/dist/pacing/au-duration.js.map +7 -0
  34. package/dist/pacing/frame-pacer.d.ts +71 -0
  35. package/dist/pacing/frame-pacer.d.ts.map +1 -0
  36. package/dist/pacing/frame-pacer.js +89 -0
  37. package/dist/pacing/frame-pacer.js.map +7 -0
  38. package/dist/pacing/index.d.ts +6 -0
  39. package/dist/pacing/index.d.ts.map +1 -0
  40. package/dist/pacing/index.js +166 -0
  41. package/dist/pacing/index.js.map +7 -0
  42. package/dist/pacing/interleave-timing.d.ts +64 -0
  43. package/dist/pacing/interleave-timing.d.ts.map +1 -0
  44. package/dist/pacing/interleave-timing.js +58 -0
  45. package/dist/pacing/interleave-timing.js.map +7 -0
  46. package/dist/video/index.d.ts +4 -0
  47. package/dist/video/index.d.ts.map +1 -0
  48. package/dist/video/index.js +247 -0
  49. package/dist/video/index.js.map +7 -0
  50. package/dist/video/keyframe-gate.d.ts +21 -0
  51. package/dist/video/keyframe-gate.d.ts.map +1 -0
  52. package/dist/video/keyframe-gate.js +39 -0
  53. package/dist/video/keyframe-gate.js.map +7 -0
  54. package/dist/video/video-render-pipeline.d.ts +60 -0
  55. package/dist/video/video-render-pipeline.d.ts.map +1 -0
  56. package/dist/video/video-render-pipeline.js +247 -0
  57. package/dist/video/video-render-pipeline.js.map +7 -0
  58. package/dist/worklet/render-worklet.js +403 -0
  59. package/dist/worklet/render-worklet.js.map +7 -0
  60. package/package.json +101 -0
  61. package/src/audio/audio-buffer-plan.ts +62 -0
  62. package/src/audio/audio-render-pipeline.ts +313 -0
  63. package/src/audio/audio-ring-buffer.ts +281 -0
  64. package/src/audio/index.ts +14 -0
  65. package/src/audio/render-messages.ts +110 -0
  66. package/src/audio/render-worklet.ts +161 -0
  67. package/src/audio/worklet-url.ts +30 -0
  68. package/src/index.ts +13 -0
  69. package/src/pacing/au-duration.ts +34 -0
  70. package/src/pacing/frame-pacer.ts +129 -0
  71. package/src/pacing/index.ts +5 -0
  72. package/src/pacing/interleave-timing.ts +95 -0
  73. package/src/video/index.ts +7 -0
  74. package/src/video/keyframe-gate.ts +35 -0
  75. package/src/video/video-render-pipeline.ts +179 -0
@@ -0,0 +1,166 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateWrapper = (obj, member, setter, getter) => ({
9
+ set _(value) {
10
+ __privateSet(obj, member, value, setter);
11
+ },
12
+ get _() {
13
+ return __privateGet(obj, member, getter);
14
+ }
15
+ });
16
+
17
+ // src/pacing/frame-pacer.ts
18
+ var _frameCount, _anchorWallMs, _anchored;
19
+ var FrameCountPacer = class {
20
+ constructor(opts) {
21
+ __privateAdd(this, _frameCount, 0);
22
+ __privateAdd(this, _anchorWallMs, 0);
23
+ __privateAdd(this, _anchored, false);
24
+ if (!(opts.frameDurationMs > 0)) {
25
+ throw new Error("frameDurationMs must be > 0");
26
+ }
27
+ this.frameDurationMs = opts.frameDurationMs;
28
+ this.thresholdMs = opts.mapper.pacingThresholdMs;
29
+ this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0);
30
+ }
31
+ /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
32
+ notifyFrame() {
33
+ if (!__privateGet(this, _anchored)) {
34
+ __privateSet(this, _anchorWallMs, now());
35
+ __privateSet(this, _anchored, true);
36
+ }
37
+ __privateWrapper(this, _frameCount)._++;
38
+ }
39
+ /**
40
+ * Compute sleep ms before reading the next MoQ object.
41
+ * Returns 0 when:
42
+ * - pacing is disabled (NullBlockMapper → threshold=0)
43
+ * - the anchor is not yet set
44
+ * - still in warmup
45
+ * - decoder is not ahead of wall clock by more than threshold
46
+ * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
47
+ */
48
+ computeSleepMs() {
49
+ if (this.thresholdMs <= 0) return 0;
50
+ if (!__privateGet(this, _anchored)) return 0;
51
+ if (__privateGet(this, _frameCount) <= this.warmupFrames) return 0;
52
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
53
+ const wallMs = now() - __privateGet(this, _anchorWallMs);
54
+ const aheadMs = mediaMs - wallMs;
55
+ if (aheadMs <= this.thresholdMs) return 0;
56
+ return aheadMs - this.thresholdMs / 2;
57
+ }
58
+ /** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
59
+ reset() {
60
+ __privateSet(this, _frameCount, 0);
61
+ __privateSet(this, _anchorWallMs, 0);
62
+ __privateSet(this, _anchored, false);
63
+ }
64
+ get snapshot() {
65
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
66
+ const wallMs = __privateGet(this, _anchored) ? now() - __privateGet(this, _anchorWallMs) : 0;
67
+ return {
68
+ frameCount: __privateGet(this, _frameCount),
69
+ mediaMs,
70
+ wallMs,
71
+ aheadMs: mediaMs - wallMs,
72
+ thresholdMs: this.thresholdMs,
73
+ anchored: __privateGet(this, _anchored)
74
+ };
75
+ }
76
+ };
77
+ _frameCount = new WeakMap();
78
+ _anchorWallMs = new WeakMap();
79
+ _anchored = new WeakMap();
80
+ function now() {
81
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
82
+ return performance.now();
83
+ }
84
+ return Date.now();
85
+ }
86
+
87
+ // src/pacing/interleave-timing.ts
88
+ var InterleaveTiming = class {
89
+ constructor(opts) {
90
+ if (!(opts.frameDurationUs > 0)) {
91
+ throw new Error("frameDurationUs must be > 0");
92
+ }
93
+ if (!Number.isFinite(opts.anchorMpuUs)) {
94
+ throw new Error("anchorMpuUs must be finite");
95
+ }
96
+ this.mapper = opts.mapper;
97
+ this.frameDurationUs = opts.frameDurationUs;
98
+ this.anchorMpuUs = opts.anchorMpuUs;
99
+ }
100
+ /**
101
+ * Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
102
+ * Per draft §8.3 + Sony anchor.
103
+ */
104
+ presentationUs(groupId, objectId) {
105
+ const coords = this.mapper.derive(groupId, objectId);
106
+ return this.anchorMpuUs + coords.ssId * this.frameDurationUs;
107
+ }
108
+ /**
109
+ * Presentation time (µs) for an AU identified by its flat SS_ID
110
+ * (multicast FEC Payload ID path).
111
+ */
112
+ presentationUsFromSsId(ssId) {
113
+ return this.anchorMpuUs + ssId * this.frameDurationUs;
114
+ }
115
+ /**
116
+ * Inverse: which (group_id, object_id) AU is scheduled at the given
117
+ * wall offset (µs) from anchor? Clamps to ssId≥0.
118
+ */
119
+ frameAtOffset(offsetUs) {
120
+ const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs));
121
+ const { sbn, esi } = this.mapper.fromSsId(rawSsId);
122
+ const spg = this.mapper.symbolsPerGroup;
123
+ const groupInBlock = Math.floor(esi / spg);
124
+ const objectId = esi - groupInBlock * spg;
125
+ const groupId = sbn * this.mapper.d + groupInBlock;
126
+ return { groupId, objectId, ssId: rawSsId };
127
+ }
128
+ /**
129
+ * Block start/end in microseconds. Source blocks span `D × GOP_duration`
130
+ * of wall time; this returns the anchored wall range [startUs, endUs).
131
+ */
132
+ blockTimeRangeUs(sbn) {
133
+ const firstSsId = sbn * this.mapper.k;
134
+ const lastSsId = (sbn + 1) * this.mapper.k;
135
+ return {
136
+ startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
137
+ endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs
138
+ };
139
+ }
140
+ };
141
+
142
+ // src/pacing/au-duration.ts
143
+ function auDurationMsFromCodec(codec, sampleRate) {
144
+ if (!(sampleRate > 0)) throw new Error("sampleRate must be > 0");
145
+ if (codec.startsWith("opus") || codec.startsWith("Opus")) {
146
+ return 960 / sampleRate * 1e3;
147
+ }
148
+ if (codec.startsWith("mp4a") || codec.startsWith("AAC") || codec.startsWith("aac")) {
149
+ return 1024 / sampleRate * 1e3;
150
+ }
151
+ if (codec.startsWith("flac") || codec.startsWith("fLaC")) {
152
+ return 4096 / sampleRate * 1e3;
153
+ }
154
+ throw new Error(`unknown audio codec for AU duration derivation: ${codec}`);
155
+ }
156
+ function pictureDurationMs(framerate) {
157
+ if (!(framerate > 0)) throw new Error("framerate must be > 0");
158
+ return 1e3 / framerate;
159
+ }
160
+ export {
161
+ FrameCountPacer,
162
+ InterleaveTiming,
163
+ auDurationMsFromCodec,
164
+ pictureDurationMs
165
+ };
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/pacing/frame-pacer.ts", "../../src/pacing/interleave-timing.ts", "../../src/pacing/au-duration.ts"],
4
+ "sourcesContent": ["/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n */\n computeSleepMs(): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n", "/**\n * InterleaveTiming \u2014 per-AU presentation-time reconstruction.\n *\n * When encoders pack multiple AUs per MMTP MPU with a single mpu_timestamp\n * (normal for FEC-interleaved audio, and for video with multi-picture MPUs),\n * the raw mpu_timestamp is ambiguous for per-AU scheduling. The FEC block\n * math (draft-ramadan-moq-fec-00 \u00A78.3) combined with the Sony libatsc3\n * pattern recovers correct per-AU time:\n *\n * au_index_in_block = groupInBlock \u00D7 symbolsPerGroup + objectId\n * ssId = sbn \u00D7 K + au_index_in_block\n * presentationUs = anchorMpuUs + ssId \u00D7 frameDurationUs\n *\n * The anchor (anchorMpuUs) is the mpu_timestamp of the first observed AU.\n * Every downstream per-AU time is then deterministic from FEC coordinates\n * + codec frame duration \u2014 identical whether the AU arrives via MoQ unicast\n * (group_id/object_id) or multicast UDP (SS_ID from FEC Payload ID).\n *\n * Consumers: CMAF segment assembler (block boundary \u2192 segment cut points),\n * debug overlays, cross-path FEC combining, sync diagnostics.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface InterleaveTimingOpts {\n /** Track's FEC mapper (provides K, D, symbolsPerGroup). */\n mapper: FecBlockMapper\n /** Nominal AU duration in microseconds. */\n frameDurationUs: number\n /** Wall-time anchor: the mpu_timestamp of the first observed AU (\u00B5s). */\n anchorMpuUs: number\n}\n\nexport class InterleaveTiming {\n readonly mapper: FecBlockMapper\n readonly frameDurationUs: number\n readonly anchorMpuUs: number\n\n constructor(opts: InterleaveTimingOpts) {\n if (!(opts.frameDurationUs > 0)) {\n throw new Error('frameDurationUs must be > 0')\n }\n if (!Number.isFinite(opts.anchorMpuUs)) {\n throw new Error('anchorMpuUs must be finite')\n }\n this.mapper = opts.mapper\n this.frameDurationUs = opts.frameDurationUs\n this.anchorMpuUs = opts.anchorMpuUs\n }\n\n /**\n * Presentation time (\u00B5s) for an AU at MoQ coords (group_id, object_id).\n * Per draft \u00A78.3 + Sony anchor.\n */\n presentationUs(groupId: number, objectId: number): number {\n const coords = this.mapper.derive(groupId, objectId)\n return this.anchorMpuUs + coords.ssId * this.frameDurationUs\n }\n\n /**\n * Presentation time (\u00B5s) for an AU identified by its flat SS_ID\n * (multicast FEC Payload ID path).\n */\n presentationUsFromSsId(ssId: number): number {\n return this.anchorMpuUs + ssId * this.frameDurationUs\n }\n\n /**\n * Inverse: which (group_id, object_id) AU is scheduled at the given\n * wall offset (\u00B5s) from anchor? Clamps to ssId\u22650.\n */\n frameAtOffset(offsetUs: number): { groupId: number; objectId: number; ssId: number } {\n const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs))\n const { sbn, esi } = this.mapper.fromSsId(rawSsId)\n // Invert \u00A78.3: esi = groupInBlock * symbolsPerGroup + objectId\n const spg = this.mapper.symbolsPerGroup\n const groupInBlock = Math.floor(esi / spg)\n const objectId = esi - groupInBlock * spg\n const groupId = sbn * this.mapper.d + groupInBlock\n return { groupId, objectId, ssId: rawSsId }\n }\n\n /**\n * Block start/end in microseconds. Source blocks span `D \u00D7 GOP_duration`\n * of wall time; this returns the anchored wall range [startUs, endUs).\n */\n blockTimeRangeUs(sbn: number): { startUs: number; endUs: number } {\n const firstSsId = sbn * this.mapper.k\n const lastSsId = (sbn + 1) * this.mapper.k\n return {\n startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,\n endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs,\n }\n }\n}\n", "/**\n * Nominal Access Unit duration helpers for common audio + video codecs.\n *\n * Kept in the pacing subpath (no mmt-container dep) so lean consumers like\n * moqtail's player.ts can import the pacer + AU duration without pulling\n * in the full audio-render-pipeline (which depends on @blockcast/mmt-container).\n */\n\n/**\n * Nominal AU duration in ms for common audio codecs at the given sample rate.\n * AAC-LC: 1024 samples. Opus (default 20ms): 960 samples @ 48kHz.\n * Throws for unknown codecs so callers supply correct timing.\n */\nexport function auDurationMsFromCodec(codec: string, sampleRate: number): number {\n if (!(sampleRate > 0)) throw new Error('sampleRate must be > 0')\n if (codec.startsWith('opus') || codec.startsWith('Opus')) {\n return (960 / sampleRate) * 1000\n }\n if (codec.startsWith('mp4a') || codec.startsWith('AAC') || codec.startsWith('aac')) {\n return (1024 / sampleRate) * 1000\n }\n if (codec.startsWith('flac') || codec.startsWith('fLaC')) {\n return (4096 / sampleRate) * 1000\n }\n throw new Error(`unknown audio codec for AU duration derivation: ${codec}`)\n}\n\n/**\n * Nominal picture duration in ms from framerate.\n */\nexport function pictureDurationMs(framerate: number): number {\n if (!(framerate > 0)) throw new Error('framerate must be > 0')\n return 1000 / framerate\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * InterleaveTiming — per-AU presentation-time reconstruction.
3
+ *
4
+ * When encoders pack multiple AUs per MMTP MPU with a single mpu_timestamp
5
+ * (normal for FEC-interleaved audio, and for video with multi-picture MPUs),
6
+ * the raw mpu_timestamp is ambiguous for per-AU scheduling. The FEC block
7
+ * math (draft-ramadan-moq-fec-00 §8.3) combined with the Sony libatsc3
8
+ * pattern recovers correct per-AU time:
9
+ *
10
+ * au_index_in_block = groupInBlock × symbolsPerGroup + objectId
11
+ * ssId = sbn × K + au_index_in_block
12
+ * presentationUs = anchorMpuUs + ssId × frameDurationUs
13
+ *
14
+ * The anchor (anchorMpuUs) is the mpu_timestamp of the first observed AU.
15
+ * Every downstream per-AU time is then deterministic from FEC coordinates
16
+ * + codec frame duration — identical whether the AU arrives via MoQ unicast
17
+ * (group_id/object_id) or multicast UDP (SS_ID from FEC Payload ID).
18
+ *
19
+ * Consumers: CMAF segment assembler (block boundary → segment cut points),
20
+ * debug overlays, cross-path FEC combining, sync diagnostics.
21
+ */
22
+ import type { FecBlockMapper } from '@blockcast/mmt-fec';
23
+ export interface InterleaveTimingOpts {
24
+ /** Track's FEC mapper (provides K, D, symbolsPerGroup). */
25
+ mapper: FecBlockMapper;
26
+ /** Nominal AU duration in microseconds. */
27
+ frameDurationUs: number;
28
+ /** Wall-time anchor: the mpu_timestamp of the first observed AU (µs). */
29
+ anchorMpuUs: number;
30
+ }
31
+ export declare class InterleaveTiming {
32
+ readonly mapper: FecBlockMapper;
33
+ readonly frameDurationUs: number;
34
+ readonly anchorMpuUs: number;
35
+ constructor(opts: InterleaveTimingOpts);
36
+ /**
37
+ * Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
38
+ * Per draft §8.3 + Sony anchor.
39
+ */
40
+ presentationUs(groupId: number, objectId: number): number;
41
+ /**
42
+ * Presentation time (µs) for an AU identified by its flat SS_ID
43
+ * (multicast FEC Payload ID path).
44
+ */
45
+ presentationUsFromSsId(ssId: number): number;
46
+ /**
47
+ * Inverse: which (group_id, object_id) AU is scheduled at the given
48
+ * wall offset (µs) from anchor? Clamps to ssId≥0.
49
+ */
50
+ frameAtOffset(offsetUs: number): {
51
+ groupId: number;
52
+ objectId: number;
53
+ ssId: number;
54
+ };
55
+ /**
56
+ * Block start/end in microseconds. Source blocks span `D × GOP_duration`
57
+ * of wall time; this returns the anchored wall range [startUs, endUs).
58
+ */
59
+ blockTimeRangeUs(sbn: number): {
60
+ startUs: number;
61
+ endUs: number;
62
+ };
63
+ }
64
+ //# sourceMappingURL=interleave-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interleave-timing.d.ts","sourceRoot":"","sources":["../../src/pacing/interleave-timing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,MAAM,EAAE,cAAc,CAAA;IACtB,2CAA2C;IAC3C,eAAe,EAAE,MAAM,CAAA;IACvB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;gBAEhB,IAAI,EAAE,oBAAoB;IAYtC;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKzD;;;OAGG;IACH,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAI5C;;;OAGG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAWpF;;;OAGG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;CAQlE"}
@@ -0,0 +1,58 @@
1
+ // src/pacing/interleave-timing.ts
2
+ var InterleaveTiming = class {
3
+ constructor(opts) {
4
+ if (!(opts.frameDurationUs > 0)) {
5
+ throw new Error("frameDurationUs must be > 0");
6
+ }
7
+ if (!Number.isFinite(opts.anchorMpuUs)) {
8
+ throw new Error("anchorMpuUs must be finite");
9
+ }
10
+ this.mapper = opts.mapper;
11
+ this.frameDurationUs = opts.frameDurationUs;
12
+ this.anchorMpuUs = opts.anchorMpuUs;
13
+ }
14
+ /**
15
+ * Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
16
+ * Per draft §8.3 + Sony anchor.
17
+ */
18
+ presentationUs(groupId, objectId) {
19
+ const coords = this.mapper.derive(groupId, objectId);
20
+ return this.anchorMpuUs + coords.ssId * this.frameDurationUs;
21
+ }
22
+ /**
23
+ * Presentation time (µs) for an AU identified by its flat SS_ID
24
+ * (multicast FEC Payload ID path).
25
+ */
26
+ presentationUsFromSsId(ssId) {
27
+ return this.anchorMpuUs + ssId * this.frameDurationUs;
28
+ }
29
+ /**
30
+ * Inverse: which (group_id, object_id) AU is scheduled at the given
31
+ * wall offset (µs) from anchor? Clamps to ssId≥0.
32
+ */
33
+ frameAtOffset(offsetUs) {
34
+ const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs));
35
+ const { sbn, esi } = this.mapper.fromSsId(rawSsId);
36
+ const spg = this.mapper.symbolsPerGroup;
37
+ const groupInBlock = Math.floor(esi / spg);
38
+ const objectId = esi - groupInBlock * spg;
39
+ const groupId = sbn * this.mapper.d + groupInBlock;
40
+ return { groupId, objectId, ssId: rawSsId };
41
+ }
42
+ /**
43
+ * Block start/end in microseconds. Source blocks span `D × GOP_duration`
44
+ * of wall time; this returns the anchored wall range [startUs, endUs).
45
+ */
46
+ blockTimeRangeUs(sbn) {
47
+ const firstSsId = sbn * this.mapper.k;
48
+ const lastSsId = (sbn + 1) * this.mapper.k;
49
+ return {
50
+ startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
51
+ endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs
52
+ };
53
+ }
54
+ };
55
+ export {
56
+ InterleaveTiming
57
+ };
58
+ //# sourceMappingURL=interleave-timing.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/pacing/interleave-timing.ts"],
4
+ "sourcesContent": ["/**\n * InterleaveTiming \u2014 per-AU presentation-time reconstruction.\n *\n * When encoders pack multiple AUs per MMTP MPU with a single mpu_timestamp\n * (normal for FEC-interleaved audio, and for video with multi-picture MPUs),\n * the raw mpu_timestamp is ambiguous for per-AU scheduling. The FEC block\n * math (draft-ramadan-moq-fec-00 \u00A78.3) combined with the Sony libatsc3\n * pattern recovers correct per-AU time:\n *\n * au_index_in_block = groupInBlock \u00D7 symbolsPerGroup + objectId\n * ssId = sbn \u00D7 K + au_index_in_block\n * presentationUs = anchorMpuUs + ssId \u00D7 frameDurationUs\n *\n * The anchor (anchorMpuUs) is the mpu_timestamp of the first observed AU.\n * Every downstream per-AU time is then deterministic from FEC coordinates\n * + codec frame duration \u2014 identical whether the AU arrives via MoQ unicast\n * (group_id/object_id) or multicast UDP (SS_ID from FEC Payload ID).\n *\n * Consumers: CMAF segment assembler (block boundary \u2192 segment cut points),\n * debug overlays, cross-path FEC combining, sync diagnostics.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface InterleaveTimingOpts {\n /** Track's FEC mapper (provides K, D, symbolsPerGroup). */\n mapper: FecBlockMapper\n /** Nominal AU duration in microseconds. */\n frameDurationUs: number\n /** Wall-time anchor: the mpu_timestamp of the first observed AU (\u00B5s). */\n anchorMpuUs: number\n}\n\nexport class InterleaveTiming {\n readonly mapper: FecBlockMapper\n readonly frameDurationUs: number\n readonly anchorMpuUs: number\n\n constructor(opts: InterleaveTimingOpts) {\n if (!(opts.frameDurationUs > 0)) {\n throw new Error('frameDurationUs must be > 0')\n }\n if (!Number.isFinite(opts.anchorMpuUs)) {\n throw new Error('anchorMpuUs must be finite')\n }\n this.mapper = opts.mapper\n this.frameDurationUs = opts.frameDurationUs\n this.anchorMpuUs = opts.anchorMpuUs\n }\n\n /**\n * Presentation time (\u00B5s) for an AU at MoQ coords (group_id, object_id).\n * Per draft \u00A78.3 + Sony anchor.\n */\n presentationUs(groupId: number, objectId: number): number {\n const coords = this.mapper.derive(groupId, objectId)\n return this.anchorMpuUs + coords.ssId * this.frameDurationUs\n }\n\n /**\n * Presentation time (\u00B5s) for an AU identified by its flat SS_ID\n * (multicast FEC Payload ID path).\n */\n presentationUsFromSsId(ssId: number): number {\n return this.anchorMpuUs + ssId * this.frameDurationUs\n }\n\n /**\n * Inverse: which (group_id, object_id) AU is scheduled at the given\n * wall offset (\u00B5s) from anchor? Clamps to ssId\u22650.\n */\n frameAtOffset(offsetUs: number): { groupId: number; objectId: number; ssId: number } {\n const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs))\n const { sbn, esi } = this.mapper.fromSsId(rawSsId)\n // Invert \u00A78.3: esi = groupInBlock * symbolsPerGroup + objectId\n const spg = this.mapper.symbolsPerGroup\n const groupInBlock = Math.floor(esi / spg)\n const objectId = esi - groupInBlock * spg\n const groupId = sbn * this.mapper.d + groupInBlock\n return { groupId, objectId, ssId: rawSsId }\n }\n\n /**\n * Block start/end in microseconds. Source blocks span `D \u00D7 GOP_duration`\n * of wall time; this returns the anchored wall range [startUs, endUs).\n */\n blockTimeRangeUs(sbn: number): { startUs: number; endUs: number } {\n const firstSsId = sbn * this.mapper.k\n const lastSsId = (sbn + 1) * this.mapper.k\n return {\n startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,\n endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs,\n }\n }\n}\n"],
5
+ "mappings": ";AAiCO,IAAM,mBAAN,MAAuB;AAAA,EAK5B,YAAY,MAA4B;AACtC,QAAI,EAAE,KAAK,kBAAkB,IAAI;AAC/B,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,QAAI,CAAC,OAAO,SAAS,KAAK,WAAW,GAAG;AACtC,YAAM,IAAI,MAAM,4BAA4B;AAAA,IAC9C;AACA,SAAK,SAAS,KAAK;AACnB,SAAK,kBAAkB,KAAK;AAC5B,SAAK,cAAc,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,SAAiB,UAA0B;AACxD,UAAM,SAAS,KAAK,OAAO,OAAO,SAAS,QAAQ;AACnD,WAAO,KAAK,cAAc,OAAO,OAAO,KAAK;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,uBAAuB,MAAsB;AAC3C,WAAO,KAAK,cAAc,OAAO,KAAK;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,UAAuE;AACnF,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,WAAW,KAAK,eAAe,CAAC;AACvE,UAAM,EAAE,KAAK,IAAI,IAAI,KAAK,OAAO,SAAS,OAAO;AAEjD,UAAM,MAAM,KAAK,OAAO;AACxB,UAAM,eAAe,KAAK,MAAM,MAAM,GAAG;AACzC,UAAM,WAAW,MAAM,eAAe;AACtC,UAAM,UAAU,MAAM,KAAK,OAAO,IAAI;AACtC,WAAO,EAAE,SAAS,UAAU,MAAM,QAAQ;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,KAAiD;AAChE,UAAM,YAAY,MAAM,KAAK,OAAO;AACpC,UAAM,YAAY,MAAM,KAAK,KAAK,OAAO;AACzC,WAAO;AAAA,MACL,SAAS,KAAK,cAAc,YAAY,KAAK;AAAA,MAC7C,OAAO,KAAK,cAAc,WAAW,KAAK;AAAA,IAC5C;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,4 @@
1
+ export { KeyframeGate } from './keyframe-gate.js';
2
+ export { VideoRenderPipeline } from './video-render-pipeline.js';
3
+ export type { VideoRenderPipelineOpts, VideoFrameMeta, VideoFrameCallback, } from './video-render-pipeline.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/video/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,YAAY,EACV,uBAAuB,EACvB,cAAc,EACd,kBAAkB,GACnB,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,247 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
+ var __privateWrapper = (obj, member, setter, getter) => ({
10
+ set _(value) {
11
+ __privateSet(obj, member, value, setter);
12
+ },
13
+ get _() {
14
+ return __privateGet(obj, member, getter);
15
+ }
16
+ });
17
+
18
+ // src/video/keyframe-gate.ts
19
+ var _open;
20
+ var KeyframeGate = class {
21
+ constructor() {
22
+ __privateAdd(this, _open, false);
23
+ }
24
+ get isOpen() {
25
+ return __privateGet(this, _open);
26
+ }
27
+ /**
28
+ * Decide whether to pass a frame through.
29
+ * Opens permanently once the first keyframe is seen.
30
+ */
31
+ allow(keyframe) {
32
+ if (__privateGet(this, _open)) return true;
33
+ if (keyframe) {
34
+ __privateSet(this, _open, true);
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ /** Re-close the gate. Next keyframe reopens it. */
40
+ reset() {
41
+ __privateSet(this, _open, false);
42
+ }
43
+ };
44
+ _open = new WeakMap();
45
+
46
+ // src/video/video-render-pipeline.ts
47
+ import { MmtpDecodePipeline } from "@blockcast/mmt-container/mmtp-decode-pipeline";
48
+
49
+ // src/pacing/frame-pacer.ts
50
+ var _frameCount, _anchorWallMs, _anchored;
51
+ var FrameCountPacer = class {
52
+ constructor(opts) {
53
+ __privateAdd(this, _frameCount, 0);
54
+ __privateAdd(this, _anchorWallMs, 0);
55
+ __privateAdd(this, _anchored, false);
56
+ if (!(opts.frameDurationMs > 0)) {
57
+ throw new Error("frameDurationMs must be > 0");
58
+ }
59
+ this.frameDurationMs = opts.frameDurationMs;
60
+ this.thresholdMs = opts.mapper.pacingThresholdMs;
61
+ this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0);
62
+ }
63
+ /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
64
+ notifyFrame() {
65
+ if (!__privateGet(this, _anchored)) {
66
+ __privateSet(this, _anchorWallMs, now());
67
+ __privateSet(this, _anchored, true);
68
+ }
69
+ __privateWrapper(this, _frameCount)._++;
70
+ }
71
+ /**
72
+ * Compute sleep ms before reading the next MoQ object.
73
+ * Returns 0 when:
74
+ * - pacing is disabled (NullBlockMapper → threshold=0)
75
+ * - the anchor is not yet set
76
+ * - still in warmup
77
+ * - decoder is not ahead of wall clock by more than threshold
78
+ * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
79
+ */
80
+ computeSleepMs() {
81
+ if (this.thresholdMs <= 0) return 0;
82
+ if (!__privateGet(this, _anchored)) return 0;
83
+ if (__privateGet(this, _frameCount) <= this.warmupFrames) return 0;
84
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
85
+ const wallMs = now() - __privateGet(this, _anchorWallMs);
86
+ const aheadMs = mediaMs - wallMs;
87
+ if (aheadMs <= this.thresholdMs) return 0;
88
+ return aheadMs - this.thresholdMs / 2;
89
+ }
90
+ /** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
91
+ reset() {
92
+ __privateSet(this, _frameCount, 0);
93
+ __privateSet(this, _anchorWallMs, 0);
94
+ __privateSet(this, _anchored, false);
95
+ }
96
+ get snapshot() {
97
+ const mediaMs = __privateGet(this, _frameCount) * this.frameDurationMs;
98
+ const wallMs = __privateGet(this, _anchored) ? now() - __privateGet(this, _anchorWallMs) : 0;
99
+ return {
100
+ frameCount: __privateGet(this, _frameCount),
101
+ mediaMs,
102
+ wallMs,
103
+ aheadMs: mediaMs - wallMs,
104
+ thresholdMs: this.thresholdMs,
105
+ anchored: __privateGet(this, _anchored)
106
+ };
107
+ }
108
+ };
109
+ _frameCount = new WeakMap();
110
+ _anchorWallMs = new WeakMap();
111
+ _anchored = new WeakMap();
112
+ function now() {
113
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
114
+ return performance.now();
115
+ }
116
+ return Date.now();
117
+ }
118
+
119
+ // src/video/video-render-pipeline.ts
120
+ function deriveVideoReorderWindow(framerate, mapper) {
121
+ const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1e3);
122
+ return mapper.d * framesPerGroup;
123
+ }
124
+ var _pacer, _gate, _opts, _decoder, _pipeline, _closed, _VideoRenderPipeline_instances, init_fn, configureDecoder_fn;
125
+ var VideoRenderPipeline = class {
126
+ constructor(opts) {
127
+ __privateAdd(this, _VideoRenderPipeline_instances);
128
+ __privateAdd(this, _pacer);
129
+ __privateAdd(this, _gate, new KeyframeGate());
130
+ __privateAdd(this, _opts);
131
+ __privateAdd(this, _decoder, null);
132
+ __privateAdd(this, _pipeline, null);
133
+ __privateAdd(this, _closed, false);
134
+ if (!(opts.framerate > 0)) throw new Error("framerate must be > 0");
135
+ __privateSet(this, _opts, opts);
136
+ __privateSet(this, _pacer, new FrameCountPacer({
137
+ frameDurationMs: 1e3 / opts.framerate,
138
+ mapper: opts.mapper,
139
+ warmupFrames: opts.warmupFrames ?? 3
140
+ }));
141
+ __privateMethod(this, _VideoRenderPipeline_instances, init_fn).call(this);
142
+ }
143
+ feedMoqObject(packet) {
144
+ __privateGet(this, _pipeline)?.feedMoqObject(packet);
145
+ }
146
+ paceSleepMs() {
147
+ return __privateGet(this, _pacer).computeSleepMs();
148
+ }
149
+ get pacerSnapshot() {
150
+ return __privateGet(this, _pacer).snapshot;
151
+ }
152
+ /** Request a fresh keyframe after a decoder error (or source switch). */
153
+ reset() {
154
+ __privateGet(this, _gate).reset();
155
+ __privateGet(this, _pacer).reset();
156
+ try {
157
+ __privateGet(this, _decoder)?.reset();
158
+ } catch {
159
+ }
160
+ __privateMethod(this, _VideoRenderPipeline_instances, configureDecoder_fn).call(this);
161
+ }
162
+ close() {
163
+ if (__privateGet(this, _closed)) return;
164
+ __privateSet(this, _closed, true);
165
+ try {
166
+ __privateGet(this, _decoder)?.close();
167
+ } catch {
168
+ }
169
+ try {
170
+ __privateGet(this, _pipeline)?.dispose();
171
+ } catch {
172
+ }
173
+ __privateSet(this, _decoder, null);
174
+ __privateSet(this, _pipeline, null);
175
+ }
176
+ };
177
+ _pacer = new WeakMap();
178
+ _gate = new WeakMap();
179
+ _opts = new WeakMap();
180
+ _decoder = new WeakMap();
181
+ _pipeline = new WeakMap();
182
+ _closed = new WeakMap();
183
+ _VideoRenderPipeline_instances = new WeakSet();
184
+ // ─── Private ─────────────────────────────────────────────────────
185
+ init_fn = function() {
186
+ __privateMethod(this, _VideoRenderPipeline_instances, configureDecoder_fn).call(this);
187
+ const pipeline = new MmtpDecodePipeline();
188
+ const reorderWindow = __privateGet(this, _opts).reorderWindow ?? deriveVideoReorderWindow(__privateGet(this, _opts).framerate, __privateGet(this, _opts).mapper);
189
+ pipeline.configure({
190
+ codec: __privateGet(this, _opts).codec,
191
+ packetId: __privateGet(this, _opts).packetId,
192
+ description: __privateGet(this, _opts).description,
193
+ reorderWindow,
194
+ framerate: __privateGet(this, _opts).framerate,
195
+ groupDurationMs: __privateGet(this, _opts).mapper.gopDurationMs,
196
+ reassemblyPolicy: __privateGet(this, _opts).reassemblyPolicy
197
+ });
198
+ if (__privateGet(this, _opts).fecClient) pipeline.setFecClient(__privateGet(this, _opts).fecClient);
199
+ pipeline.onFrame((codecData, meta) => {
200
+ if (__privateGet(this, _closed) || !__privateGet(this, _decoder)) return;
201
+ if (codecData.byteLength === 0) return;
202
+ if (!__privateGet(this, _gate).allow(meta.keyframe)) return;
203
+ __privateGet(this, _pacer).notifyFrame();
204
+ try {
205
+ __privateGet(this, _decoder).decode(new EncodedVideoChunk({
206
+ type: meta.keyframe ? "key" : "delta",
207
+ data: codecData,
208
+ timestamp: meta.timestamp
209
+ }));
210
+ } catch (err) {
211
+ __privateGet(this, _opts).onError?.(err);
212
+ this.reset();
213
+ }
214
+ });
215
+ __privateSet(this, _pipeline, pipeline);
216
+ };
217
+ configureDecoder_fn = function() {
218
+ try {
219
+ __privateGet(this, _decoder)?.close();
220
+ } catch {
221
+ }
222
+ const userMeta = { timestamp: 0, keyframe: false, packetId: 0 };
223
+ const decoder = new VideoDecoder({
224
+ output: (frame) => {
225
+ userMeta.timestamp = frame.timestamp;
226
+ __privateGet(this, _opts).onFrame(frame, userMeta);
227
+ },
228
+ error: (err) => {
229
+ __privateGet(this, _opts).onError?.(err);
230
+ __privateGet(this, _gate).reset();
231
+ }
232
+ });
233
+ const config = {
234
+ codec: __privateGet(this, _opts).codec,
235
+ codedWidth: __privateGet(this, _opts).width,
236
+ codedHeight: __privateGet(this, _opts).height,
237
+ ...__privateGet(this, _opts).description ? { description: __privateGet(this, _opts).description } : {},
238
+ ...__privateGet(this, _opts).hardwareAcceleration ? { hardwareAcceleration: __privateGet(this, _opts).hardwareAcceleration } : {}
239
+ };
240
+ decoder.configure(config);
241
+ __privateSet(this, _decoder, decoder);
242
+ };
243
+ export {
244
+ KeyframeGate,
245
+ VideoRenderPipeline
246
+ };
247
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/video/keyframe-gate.ts", "../../src/video/video-render-pipeline.ts", "../../src/pacing/frame-pacer.ts"],
4
+ "sourcesContent": ["/**\n * KeyframeGate \u2014 drops delta frames until a keyframe arrives.\n *\n * Required when subscribing mid-stream: the relay may deliver P/B frames\n * from the middle of a GOP before the first IDR. Feeding those into\n * VideoDecoder before an init yields \"key frame required\" errors.\n *\n * Call reset() to re-arm the gate after a decoder error (requestKeyframe).\n */\n\nexport class KeyframeGate {\n #open = false\n\n get isOpen(): boolean {\n return this.#open\n }\n\n /**\n * Decide whether to pass a frame through.\n * Opens permanently once the first keyframe is seen.\n */\n allow(keyframe: boolean): boolean {\n if (this.#open) return true\n if (keyframe) {\n this.#open = true\n return true\n }\n return false\n }\n\n /** Re-close the gate. Next keyframe reopens it. */\n reset(): void {\n this.#open = false\n }\n}\n", "/**\n * VideoRenderPipeline \u2014 end-to-end video render for MoQ/MMT players.\n *\n * Pipeline: MoQ object \u2192 MmtpDecodePipeline \u2192 keyframe gate \u2192 WebCodecs\n * VideoDecoder \u2192 consumer's onFrame(VideoFrame).\n *\n * Replaces:\n * - moqtail's `decodeQueueSize > 10 drop-deltas` hack in webcodecs-backend.ts\n * - hang-mmt-fec's \"reorderWindow = 0 // TODO\" in video/decoder.ts\n *\n * Pacing uses FrameCountPacer (anchor on first decoded picture, frame count \u00D7\n * 1/fps) with threshold = mapper.pacingThresholdMs. Reorder window comes from\n * mapper.d (groups per FEC block), which is the natural bound for out-of-order\n * delivery within a block after converting catalog groups to frame count.\n */\n\nimport { MmtpDecodePipeline, type PipelineFecClient } from '@blockcast/mmt-container/mmtp-decode-pipeline'\nimport type { MfuReassemblyPolicy } from '@blockcast/mmt-container/mfu-reassembly-policy'\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\nimport { FrameCountPacer } from '../pacing/frame-pacer.js'\nimport { KeyframeGate } from './keyframe-gate.js'\n\nexport interface VideoFrameMeta {\n timestamp: number\n keyframe: boolean\n packetId: number\n}\n\nexport type VideoFrameCallback = (frame: VideoFrame, meta: VideoFrameMeta) => void\n\nexport function deriveVideoReorderWindow(\n framerate: number,\n mapper: Pick<FecBlockMapper, 'd' | 'gopDurationMs'>,\n): number {\n const framesPerGroup = Math.ceil(framerate * mapper.gopDurationMs / 1000)\n return mapper.d * framesPerGroup\n}\n\nexport interface VideoRenderPipelineOpts {\n codec: string\n /** MMTP packet_id for this video track \u2014 from catalog. */\n packetId?: number\n description?: Uint8Array\n width: number\n height: number\n /** Frames per second (used for pacing + default reorder window). */\n framerate: number\n mapper: FecBlockMapper\n fecClient?: PipelineFecClient\n /** Reliable MoQ is clock-free; datagram deadlines come from catalog/in-band timing. */\n reassemblyPolicy: MfuReassemblyPolicy\n /** Receives VideoFrames ready to render. */\n onFrame: VideoFrameCallback\n /** Called on decoder errors. */\n onError?: (err: unknown) => void\n /** Reorder window in frames. Defaults to one catalog-derived FEC block. */\n reorderWindow?: number\n /** Warmup frames before pacing engages. Default 3 (absorbs decoder init jitter). */\n warmupFrames?: number\n /** Optional decoder hardware acceleration hint. */\n hardwareAcceleration?: HardwareAcceleration\n}\n\nexport class VideoRenderPipeline {\n readonly #pacer: FrameCountPacer\n readonly #gate = new KeyframeGate()\n readonly #opts: VideoRenderPipelineOpts\n\n #decoder: VideoDecoder | null = null\n #pipeline: MmtpDecodePipeline | null = null\n #closed = false\n\n constructor(opts: VideoRenderPipelineOpts) {\n if (!(opts.framerate > 0)) throw new Error('framerate must be > 0')\n this.#opts = opts\n this.#pacer = new FrameCountPacer({\n frameDurationMs: 1000 / opts.framerate,\n mapper: opts.mapper,\n warmupFrames: opts.warmupFrames ?? 3,\n })\n this.#init()\n }\n\n feedMoqObject(packet: Uint8Array): void {\n this.#pipeline?.feedMoqObject(packet)\n }\n\n paceSleepMs(): number {\n return this.#pacer.computeSleepMs()\n }\n\n get pacerSnapshot() {\n return this.#pacer.snapshot\n }\n\n /** Request a fresh keyframe after a decoder error (or source switch). */\n reset(): void {\n this.#gate.reset()\n this.#pacer.reset()\n try { this.#decoder?.reset() } catch { /* may be closed */ }\n // Re-configure so the decoder is ready for the next keyframe.\n this.#configureDecoder()\n }\n\n close(): void {\n if (this.#closed) return\n this.#closed = true\n try { this.#decoder?.close() } catch { /* noop */ }\n try { this.#pipeline?.dispose() } catch { /* noop */ }\n this.#decoder = null\n this.#pipeline = null\n }\n\n // \u2500\u2500\u2500 Private \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n #init(): void {\n this.#configureDecoder()\n\n const pipeline = new MmtpDecodePipeline()\n const reorderWindow = this.#opts.reorderWindow\n ?? deriveVideoReorderWindow(this.#opts.framerate, this.#opts.mapper)\n pipeline.configure({\n codec: this.#opts.codec,\n packetId: this.#opts.packetId,\n description: this.#opts.description,\n reorderWindow,\n framerate: this.#opts.framerate,\n groupDurationMs: this.#opts.mapper.gopDurationMs,\n reassemblyPolicy: this.#opts.reassemblyPolicy,\n })\n if (this.#opts.fecClient) pipeline.setFecClient(this.#opts.fecClient)\n\n pipeline.onFrame((codecData, meta) => {\n if (this.#closed || !this.#decoder) return\n if (codecData.byteLength === 0) return\n if (!this.#gate.allow(meta.keyframe)) return\n\n this.#pacer.notifyFrame()\n try {\n this.#decoder.decode(new EncodedVideoChunk({\n type: meta.keyframe ? 'key' : 'delta',\n data: codecData,\n timestamp: meta.timestamp,\n }))\n } catch (err) {\n this.#opts.onError?.(err)\n this.reset()\n }\n })\n this.#pipeline = pipeline\n }\n\n #configureDecoder(): void {\n try { this.#decoder?.close() } catch { /* noop */ }\n\n const userMeta: VideoFrameMeta = { timestamp: 0, keyframe: false, packetId: 0 }\n const decoder = new VideoDecoder({\n output: (frame) => {\n // WebCodecs VideoFrame doesn't carry our frame meta; the consumer\n // rematches via frame.timestamp if needed.\n userMeta.timestamp = frame.timestamp\n this.#opts.onFrame(frame, userMeta)\n },\n error: (err) => {\n this.#opts.onError?.(err)\n this.#gate.reset()\n },\n })\n const config: VideoDecoderConfig = {\n codec: this.#opts.codec,\n codedWidth: this.#opts.width,\n codedHeight: this.#opts.height,\n ...(this.#opts.description ? { description: this.#opts.description } : {}),\n ...(this.#opts.hardwareAcceleration ? { hardwareAcceleration: this.#opts.hardwareAcceleration } : {}),\n }\n decoder.configure(config)\n this.#decoder = decoder\n }\n}\n", "/**\n * FrameCountPacer \u2014 media-agnostic wall-clock pacer for MMT A/V decode loops.\n *\n * Why not use MPU timestamps? Under FEC interleaving, encoders pack multiple\n * Access Units (AUs) per MMTP MPU with a single mpu_timestamp. The observed\n * timestamp delta between MPUs is a fraction of the real per-AU duration\n * (e.g. 10.22ms between MPUs for 21.33ms AAC frames at D=2 interleave).\n * Pacing on that delta sees \"media \u2248 wall\" and never sleeps, letting burst\n * relay delivery saturate the decoder / ring buffer.\n *\n * The Sony libatsc3 pattern (atsc3_mmt_context_mfu_depacketizer) solves this\n * by anchoring wall-clock time to the first received AU and counting AUs:\n * media_elapsed_ms = au_count \u00D7 nominal_au_duration_ms\n * The MPU timestamp is used elsewhere as an absolute-time anchor, never as\n * an incremental clock.\n *\n * The sleep threshold comes from the catalog's FEC interleaveDepthMs (via\n * FecBlockMapper.pacingThresholdMs). When no FEC is configured, a\n * NullBlockMapper yields threshold=0 and this pacer never sleeps.\n *\n * One pacer instance = one track (e.g. audio, video) on one player.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\nexport interface FrameCountPacerOpts {\n /** Required: AU duration (audio) or picture interval (video), in ms. */\n frameDurationMs: number\n /** FEC mapper from @blockcast/mmt-fec. NullBlockMapper disables pacing. */\n mapper: FecBlockMapper\n /**\n * Number of frames to let through before pacing starts. Lets startup\n * burst drain populate the ring buffer / decoder queue. Default 0.\n */\n warmupFrames?: number\n}\n\nexport interface PacerSnapshot {\n /** Frames observed (from notifyFrame). */\n frameCount: number\n /** Computed media time: frameCount \u00D7 frameDurationMs. */\n mediaMs: number\n /** Wall-clock time since anchor. */\n wallMs: number\n /** mediaMs - wallMs. Positive \u2192 decoder ahead of realtime. */\n aheadMs: number\n /** Pacing threshold (ms) from the FEC mapper. */\n thresholdMs: number\n /** Has the first frame been observed (wall anchor set)? */\n anchored: boolean\n}\n\nexport class FrameCountPacer {\n readonly frameDurationMs: number\n readonly thresholdMs: number\n readonly warmupFrames: number\n\n #frameCount = 0\n #anchorWallMs = 0\n #anchored = false\n\n constructor(opts: FrameCountPacerOpts) {\n if (!(opts.frameDurationMs > 0)) {\n throw new Error('frameDurationMs must be > 0')\n }\n this.frameDurationMs = opts.frameDurationMs\n this.thresholdMs = opts.mapper.pacingThresholdMs\n this.warmupFrames = Math.max(0, opts.warmupFrames ?? 0)\n }\n\n /** Call on every emitted decoded frame (AU or picture). Anchors on first call. */\n notifyFrame(): void {\n if (!this.#anchored) {\n this.#anchorWallMs = now()\n this.#anchored = true\n }\n this.#frameCount++\n }\n\n /**\n * Compute sleep ms before reading the next MoQ object.\n * Returns 0 when:\n * - pacing is disabled (NullBlockMapper \u2192 threshold=0)\n * - the anchor is not yet set\n * - still in warmup\n * - decoder is not ahead of wall clock by more than threshold\n * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).\n */\n computeSleepMs(): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = now() - this.#anchorWallMs\n const aheadMs = mediaMs - wallMs\n if (aheadMs <= this.thresholdMs) return 0\n return aheadMs - this.thresholdMs / 2\n }\n\n /** Reset anchor + counter. Call on source switch (MoQ \u2194 multicast). */\n reset(): void {\n this.#frameCount = 0\n this.#anchorWallMs = 0\n this.#anchored = false\n }\n\n get snapshot(): PacerSnapshot {\n const mediaMs = this.#frameCount * this.frameDurationMs\n const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0\n return {\n frameCount: this.#frameCount,\n mediaMs,\n wallMs,\n aheadMs: mediaMs - wallMs,\n thresholdMs: this.thresholdMs,\n anchored: this.#anchored,\n }\n }\n}\n\nfunction now(): number {\n // performance.now() is available in main thread, workers, and AudioWorklet.\n // Fall back to Date.now() for exotic runtimes (shouldn't happen in browsers).\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now()\n }\n return Date.now()\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * KeyframeGate — drops delta frames until a keyframe arrives.
3
+ *
4
+ * Required when subscribing mid-stream: the relay may deliver P/B frames
5
+ * from the middle of a GOP before the first IDR. Feeding those into
6
+ * VideoDecoder before an init yields "key frame required" errors.
7
+ *
8
+ * Call reset() to re-arm the gate after a decoder error (requestKeyframe).
9
+ */
10
+ export declare class KeyframeGate {
11
+ #private;
12
+ get isOpen(): boolean;
13
+ /**
14
+ * Decide whether to pass a frame through.
15
+ * Opens permanently once the first keyframe is seen.
16
+ */
17
+ allow(keyframe: boolean): boolean;
18
+ /** Re-close the gate. Next keyframe reopens it. */
19
+ reset(): void;
20
+ }
21
+ //# sourceMappingURL=keyframe-gate.d.ts.map