@blockcast/mmt-render 0.1.0 → 0.3.0-main.1cd3cdea89da

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 (35) hide show
  1. package/README.md +8 -19
  2. package/dist/.build-stamp +0 -0
  3. package/dist/audio/audio-render-pipeline.js +217 -248
  4. package/dist/audio/audio-render-pipeline.js.map +2 -2
  5. package/dist/audio/audio-ring-buffer.js +93 -127
  6. package/dist/audio/audio-ring-buffer.js.map +1 -1
  7. package/dist/audio/index.js +313 -361
  8. package/dist/audio/index.js.map +3 -3
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/pacing/frame-pacer.d.ts +22 -1
  12. package/dist/pacing/frame-pacer.d.ts.map +1 -1
  13. package/dist/pacing/frame-pacer.js +43 -39
  14. package/dist/pacing/frame-pacer.js.map +2 -2
  15. package/dist/pacing/index.d.ts +0 -2
  16. package/dist/pacing/index.d.ts.map +1 -1
  17. package/dist/pacing/index.js +43 -95
  18. package/dist/pacing/index.js.map +3 -3
  19. package/dist/video/index.js +129 -139
  20. package/dist/video/index.js.map +2 -2
  21. package/dist/video/keyframe-gate.js +5 -17
  22. package/dist/video/keyframe-gate.js.map +1 -1
  23. package/dist/video/video-render-pipeline.js +129 -139
  24. package/dist/video/video-render-pipeline.js.map +2 -2
  25. package/dist/worklet/render-worklet.js +171 -219
  26. package/dist/worklet/render-worklet.js.map +2 -2
  27. package/package.json +4 -8
  28. package/src/index.ts +1 -1
  29. package/src/pacing/frame-pacer.ts +28 -1
  30. package/src/pacing/index.ts +0 -2
  31. package/dist/pacing/interleave-timing.d.ts +0 -64
  32. package/dist/pacing/interleave-timing.d.ts.map +0 -1
  33. package/dist/pacing/interleave-timing.js +0 -58
  34. package/dist/pacing/interleave-timing.js.map +0 -7
  35. package/src/pacing/interleave-timing.ts +0 -95
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blockcast/mmt-render",
3
- "version": "0.1.0",
4
- "description": "Shared MMT render pipeline (FEC-aware pacing, interleave timing, audio worklet) for MoQ players",
3
+ "version": "0.3.0-main.1cd3cdea89da",
4
+ "description": "Shared MMT render pipeline (FEC-aware pacing, audio worklet, video render) for MoQ players",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -18,10 +18,6 @@
18
18
  "import": "./dist/pacing/frame-pacer.js",
19
19
  "types": "./dist/pacing/frame-pacer.d.ts"
20
20
  },
21
- "./pacing/interleave-timing": {
22
- "import": "./dist/pacing/interleave-timing.js",
23
- "types": "./dist/pacing/interleave-timing.d.ts"
24
- },
25
21
  "./audio": {
26
22
  "import": "./dist/audio/index.js",
27
23
  "types": "./dist/audio/index.d.ts"
@@ -73,8 +69,8 @@
73
69
  "test:watch": "vitest"
74
70
  },
75
71
  "dependencies": {
76
- "@blockcast/mmt-container": "^0.1.0",
77
- "@blockcast/mmt-fec": "^0.1.0"
72
+ "@blockcast/mmt-container": "0.4.0-main.1cd3cdea89da",
73
+ "@blockcast/mmt-fec": "0.3.0-main.1cd3cdea89da"
78
74
  },
79
75
  "devDependencies": {
80
76
  "@types/audioworklet": "^0.0.77",
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @blockcast/mmt-render — shared MMT render pipeline for MoQ players.
3
3
  *
4
4
  * Subpath exports:
5
- * - ./pacing FrameCountPacer, InterleaveTiming
5
+ * - ./pacing FrameCountPacer
6
6
  * - ./audio AudioRenderPipeline, AudioRingBuffer, worklet helpers
7
7
  * - ./video VideoRenderPipeline, KeyframeGate
8
8
  * - ./audio/worklet.js Prebuilt self-contained AudioWorklet bundle
@@ -83,13 +83,40 @@ export class FrameCountPacer {
83
83
  * - pacing is disabled (NullBlockMapper → threshold=0)
84
84
  * - the anchor is not yet set
85
85
  * - still in warmup
86
+ * - the caller reports a standing backlog (see `observedDelayMs`)
86
87
  * - decoder is not ahead of wall clock by more than threshold
87
88
  * Otherwise returns aheadMs - thresholdMs/2 (leaves half the threshold as headroom).
89
+ *
90
+ * `observedDelayMs` is how far this frame is behind the producer's own clock,
91
+ * measured by the caller against a producer-supplied sending instant (for
92
+ * MMTP, the header send-wall timestamp). It is optional because the anchored
93
+ * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST
94
+ * frame read, so `aheadMs` measures consumption against the join instant and
95
+ * not against the producer. Once `aheadMs` reaches the threshold the loop
96
+ * consumes at exactly the source rate, and recovering latency present at join
97
+ * requires consuming FASTER than the source rate. So without this argument a
98
+ * consumer that joined a live stream mid-block keeps that join latency for the
99
+ * life of the subscription.
100
+ *
101
+ * A caller that can measure the real distance from the producer passes it and
102
+ * gets a drain: while the backlog exceeds the threshold there is nothing to
103
+ * pace against, so return 0 and let the transport set the rate. At the live
104
+ * edge the anchored-rate behavior below applies unchanged.
105
+ *
106
+ * Callers with no producer clock omit it and keep the pure ATSC3-style
107
+ * anchored pacing (see the class doc), where a first-frame anchor is correct
108
+ * because a broadcast push has no live edge to catch up to.
88
109
  */
89
- computeSleepMs(): number {
110
+ computeSleepMs(observedDelayMs?: number): number {
90
111
  if (this.thresholdMs <= 0) return 0
91
112
  if (!this.#anchored) return 0
92
113
  if (this.#frameCount <= this.warmupFrames) return 0
114
+ // Only a finite measurement can license the drain. NaN/Infinity mean the
115
+ // caller could not measure the distance, which is not the same claim as
116
+ // "the consumer is at the edge", so fall through to anchored pacing.
117
+ if (observedDelayMs !== undefined && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {
118
+ return 0
119
+ }
93
120
 
94
121
  const mediaMs = this.#frameCount * this.frameDurationMs
95
122
  const wallMs = now() - this.#anchorWallMs
@@ -1,5 +1,3 @@
1
1
  export { FrameCountPacer } from './frame-pacer.js'
2
2
  export type { FrameCountPacerOpts, PacerSnapshot } from './frame-pacer.js'
3
- export { InterleaveTiming } from './interleave-timing.js'
4
- export type { InterleaveTimingOpts } from './interleave-timing.js'
5
3
  export { auDurationMsFromCodec, pictureDurationMs } from './au-duration.js'
@@ -1,64 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,58 +0,0 @@
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
@@ -1,7 +0,0 @@
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
- }
@@ -1,95 +0,0 @@
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
-
23
- import type { FecBlockMapper } from '@blockcast/mmt-fec'
24
-
25
- export interface InterleaveTimingOpts {
26
- /** Track's FEC mapper (provides K, D, symbolsPerGroup). */
27
- mapper: FecBlockMapper
28
- /** Nominal AU duration in microseconds. */
29
- frameDurationUs: number
30
- /** Wall-time anchor: the mpu_timestamp of the first observed AU (µs). */
31
- anchorMpuUs: number
32
- }
33
-
34
- export class InterleaveTiming {
35
- readonly mapper: FecBlockMapper
36
- readonly frameDurationUs: number
37
- readonly anchorMpuUs: number
38
-
39
- constructor(opts: InterleaveTimingOpts) {
40
- if (!(opts.frameDurationUs > 0)) {
41
- throw new Error('frameDurationUs must be > 0')
42
- }
43
- if (!Number.isFinite(opts.anchorMpuUs)) {
44
- throw new Error('anchorMpuUs must be finite')
45
- }
46
- this.mapper = opts.mapper
47
- this.frameDurationUs = opts.frameDurationUs
48
- this.anchorMpuUs = opts.anchorMpuUs
49
- }
50
-
51
- /**
52
- * Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
53
- * Per draft §8.3 + Sony anchor.
54
- */
55
- presentationUs(groupId: number, objectId: number): number {
56
- const coords = this.mapper.derive(groupId, objectId)
57
- return this.anchorMpuUs + coords.ssId * this.frameDurationUs
58
- }
59
-
60
- /**
61
- * Presentation time (µs) for an AU identified by its flat SS_ID
62
- * (multicast FEC Payload ID path).
63
- */
64
- presentationUsFromSsId(ssId: number): number {
65
- return this.anchorMpuUs + ssId * this.frameDurationUs
66
- }
67
-
68
- /**
69
- * Inverse: which (group_id, object_id) AU is scheduled at the given
70
- * wall offset (µs) from anchor? Clamps to ssId≥0.
71
- */
72
- frameAtOffset(offsetUs: number): { groupId: number; objectId: number; ssId: number } {
73
- const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs))
74
- const { sbn, esi } = this.mapper.fromSsId(rawSsId)
75
- // Invert §8.3: esi = groupInBlock * symbolsPerGroup + objectId
76
- const spg = this.mapper.symbolsPerGroup
77
- const groupInBlock = Math.floor(esi / spg)
78
- const objectId = esi - groupInBlock * spg
79
- const groupId = sbn * this.mapper.d + groupInBlock
80
- return { groupId, objectId, ssId: rawSsId }
81
- }
82
-
83
- /**
84
- * Block start/end in microseconds. Source blocks span `D × GOP_duration`
85
- * of wall time; this returns the anchored wall range [startUs, endUs).
86
- */
87
- blockTimeRangeUs(sbn: number): { startUs: number; endUs: number } {
88
- const firstSsId = sbn * this.mapper.k
89
- const lastSsId = (sbn + 1) * this.mapper.k
90
- return {
91
- startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
92
- endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs,
93
- }
94
- }
95
- }