@blockcast/mmt-render 0.3.0-main.3f0b3688e8b7 → 0.3.0-main.45cce9806486
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pacing/index.d.ts +0 -2
- package/dist/pacing/index.d.ts.map +1 -1
- package/dist/pacing/index.js +0 -67
- package/dist/pacing/index.js.map +3 -3
- package/package.json +4 -8
- package/src/index.ts +1 -1
- package/src/pacing/index.ts +0 -2
- package/dist/pacing/interleave-timing.d.ts +0 -73
- package/dist/pacing/interleave-timing.d.ts.map +0 -1
- package/dist/pacing/interleave-timing.js +0 -69
- package/dist/pacing/interleave-timing.js.map +0 -7
- package/src/pacing/interleave-timing.ts +0 -122
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @blockcast/mmt-render
|
|
2
2
|
|
|
3
|
-
Shared render pipeline for MMT players: FEC-aware wall-clock pacing,
|
|
3
|
+
Shared render pipeline for MMT players: FEC-aware wall-clock pacing, an `AudioWorkletProcessor` with an exact-timestamp PCM FIFO, and a keyframe-gated `VideoDecoder` driver. The package is the canonical substrate for moqtail, `hang-mmt-fec`, and Shaka integrations so they do not independently invent timing, buffering, or sample-correction policy. It is *not* a "reference renderer + diagnostics UI" — there is no DOM, chart, or catalog browser. Diagnostics are structured snapshots and worklet telemetry messages that callers can render however they like.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -17,8 +17,9 @@ import { AudioRenderPipeline } from "@blockcast/mmt-render/audio"
|
|
|
17
17
|
import { FecBlockMapper } from "@blockcast/mmt-fec"
|
|
18
18
|
|
|
19
19
|
const ctx = new AudioContext({ sampleRate: 48000 })
|
|
20
|
-
// blockProfile is the catalog `fec.blockProfile`: required, never defaulted
|
|
21
|
-
|
|
20
|
+
// blockProfile is the catalog `fec.blockProfile`: required, never defaulted,
|
|
21
|
+
// and "iso-ssbg1-v1" is the only profile (the retired "k-padded-v1" is refused).
|
|
22
|
+
const mapper = new FecBlockMapper({ blockProfile: "iso-ssbg1-v1", interleaveDepthMs: 66, gopDurationMs: 21.33, k: 32 })
|
|
22
23
|
|
|
23
24
|
const audio = new AudioRenderPipeline({
|
|
24
25
|
context: ctx,
|
|
@@ -79,9 +80,8 @@ Per `package.json`:
|
|
|
79
80
|
| Subpath | Symbols |
|
|
80
81
|
|---------|---------|
|
|
81
82
|
| `@blockcast/mmt-render` | re-exports of `./pacing`, `./audio`, `./video` |
|
|
82
|
-
| `@blockcast/mmt-render/pacing` | `FrameCountPacer`, `
|
|
83
|
+
| `@blockcast/mmt-render/pacing` | `FrameCountPacer`, `auDurationMsFromCodec`, `pictureDurationMs` |
|
|
83
84
|
| `@blockcast/mmt-render/pacing/frame-pacer` | `FrameCountPacer` only |
|
|
84
|
-
| `@blockcast/mmt-render/pacing/interleave-timing` | `InterleaveTiming` only |
|
|
85
85
|
| `@blockcast/mmt-render/audio` | `AudioRenderPipeline`, `AudioRingBuffer`, message types, `WORKLET_PROCESSOR_NAME`, `workletModuleUrl` |
|
|
86
86
|
| `@blockcast/mmt-render/audio/ring-buffer` | `AudioRingBuffer` only |
|
|
87
87
|
| `@blockcast/mmt-render/audio/render-messages` | `FromMain` / `ToMain` message types |
|
|
@@ -102,18 +102,6 @@ Media-agnostic wall-clock pacer. Anchors on the first `notifyFrame()` call; comp
|
|
|
102
102
|
|
|
103
103
|
`PacerSnapshot` exposes `frameCount`, `mediaMs`, `wallMs`, `aheadMs`, `thresholdMs`, `anchored` for diagnostics overlays.
|
|
104
104
|
|
|
105
|
-
### `InterleaveTiming` (`src/pacing/interleave-timing.ts`)
|
|
106
|
-
|
|
107
|
-
Per-AU presentation-time reconstruction from FEC coordinates: the `k-padded-v1` K-stride pacing model of `draft-ramadan-moq-fec-00 §8.3`, with `symbolsPerGroup = ceil(K / D)` computed here. It is pacing arithmetic, not an FEC block locator, and refuses a mapper under any other `fec.blockProfile`:
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
au_index_in_block = groupInBlock × symbolsPerGroup + objectId
|
|
111
|
-
ssId = sbn × K + au_index_in_block
|
|
112
|
-
presentationUs = anchorMpuUs + ssId × frameDurationUs
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Same answer whether the AU arrived via MoQ unicast (group_id/object_id) or multicast UDP (SS_ID from the FEC Payload ID). Methods: `presentationUs(groupId, objectId)`, `presentationUsFromSsId(ssId)`, `frameAtOffset(offsetUs)` (inverse), `blockTimeRangeUs(sbn)`. Consumers: CMAF segment cut points, debug overlays, cross-path FEC combining.
|
|
116
|
-
|
|
117
105
|
### `auDurationMsFromCodec` / `pictureDurationMs` (`src/pacing/au-duration.ts`)
|
|
118
106
|
|
|
119
107
|
Nominal AU duration helpers. AAC-LC = 1024 samples; Opus default = 960; FLAC = 4096. Throws on unknown codec — no silent fallback. Lives in the pacing subpath specifically to avoid pulling in `mmt-container`.
|
|
@@ -201,7 +189,7 @@ pnpm test:watch
|
|
|
201
189
|
pnpm build # tsc --emitDeclarationOnly + esbuild bundles (incl. worklet IIFE)
|
|
202
190
|
```
|
|
203
191
|
|
|
204
|
-
Existing suites: `frame-pacer.test.ts`, `
|
|
192
|
+
Existing suites: `frame-pacer.test.ts`, `audio-ring-buffer.test.ts`, `keyframe-gate.test.ts`. The pacer suite uses `vi.spyOn(performance, "now")` to drive deterministic wall time. There is no DOM / `AudioContext` integration test — the worklet is exercised in-browser by the consuming players' E2E suites.
|
|
205
193
|
|
|
206
194
|
## License
|
|
207
195
|
|
package/dist/index.d.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
|
|
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
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @blockcast/mmt-render \u2014 shared MMT render pipeline for MoQ players.\n *\n * Subpath exports:\n * - ./pacing FrameCountPacer
|
|
4
|
+
"sourcesContent": ["/**\n * @blockcast/mmt-render \u2014 shared MMT render pipeline for MoQ players.\n *\n * Subpath exports:\n * - ./pacing FrameCountPacer\n * - ./audio AudioRenderPipeline, AudioRingBuffer, worklet helpers\n * - ./video VideoRenderPipeline, KeyframeGate\n * - ./audio/worklet.js Prebuilt self-contained AudioWorklet bundle\n */\n\nexport * from './pacing/index.js'\nexport * from './audio/index.js'\nexport * from './video/index.js'\n"],
|
|
5
5
|
"mappings": ";AAUA,cAAc;AACd,cAAc;AACd,cAAc;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/pacing/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
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';
|
|
6
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pacing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pacing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/pacing/index.js
CHANGED
|
@@ -88,72 +88,6 @@ function now() {
|
|
|
88
88
|
return Date.now();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// src/pacing/interleave-timing.ts
|
|
92
|
-
var SS_ID_SPAN = 4294967296;
|
|
93
|
-
var InterleaveTiming = class {
|
|
94
|
-
constructor(opts) {
|
|
95
|
-
if (!(opts.frameDurationUs > 0)) {
|
|
96
|
-
throw new Error("frameDurationUs must be > 0");
|
|
97
|
-
}
|
|
98
|
-
if (!Number.isFinite(opts.anchorMpuUs)) {
|
|
99
|
-
throw new Error("anchorMpuUs must be finite");
|
|
100
|
-
}
|
|
101
|
-
if (opts.mapper.blockProfile !== "k-padded-v1") {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`InterleaveTiming is the k-padded-v1 K-stride pacing model; a ${opts.mapper.blockProfile ?? "non-FEC"} stream has no K-stride block geometry`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
this.mapper = opts.mapper;
|
|
107
|
-
this.symbolsPerGroup = Math.ceil(opts.mapper.k / opts.mapper.d);
|
|
108
|
-
this.frameDurationUs = opts.frameDurationUs;
|
|
109
|
-
this.anchorMpuUs = opts.anchorMpuUs;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
|
|
113
|
-
* Per draft §8.3 + Sony anchor.
|
|
114
|
-
*/
|
|
115
|
-
presentationUs(groupId, objectId) {
|
|
116
|
-
const { k, d } = this.mapper;
|
|
117
|
-
const sbn = Math.floor(groupId / d);
|
|
118
|
-
const esi = (groupId - sbn * d) * this.symbolsPerGroup + objectId;
|
|
119
|
-
const ssId = (sbn % SS_ID_SPAN * k + esi) % SS_ID_SPAN;
|
|
120
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Presentation time (µs) for an AU identified by its flat SS_ID
|
|
124
|
-
* (multicast FEC Payload ID path).
|
|
125
|
-
*/
|
|
126
|
-
presentationUsFromSsId(ssId) {
|
|
127
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Inverse: which (group_id, object_id) AU is scheduled at the given
|
|
131
|
-
* wall offset (µs) from anchor? Clamps to ssId≥0.
|
|
132
|
-
*/
|
|
133
|
-
frameAtOffset(offsetUs) {
|
|
134
|
-
const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs));
|
|
135
|
-
const sbn = Math.floor(rawSsId / this.mapper.k);
|
|
136
|
-
const esi = rawSsId % this.mapper.k;
|
|
137
|
-
const spg = this.symbolsPerGroup;
|
|
138
|
-
const groupInBlock = Math.floor(esi / spg);
|
|
139
|
-
const objectId = esi - groupInBlock * spg;
|
|
140
|
-
const groupId = sbn * this.mapper.d + groupInBlock;
|
|
141
|
-
return { groupId, objectId, ssId: rawSsId };
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Block start/end in microseconds. Source blocks span `D × GOP_duration`
|
|
145
|
-
* of wall time; this returns the anchored wall range [startUs, endUs).
|
|
146
|
-
*/
|
|
147
|
-
blockTimeRangeUs(sbn) {
|
|
148
|
-
const firstSsId = sbn * this.mapper.k;
|
|
149
|
-
const lastSsId = (sbn + 1) * this.mapper.k;
|
|
150
|
-
return {
|
|
151
|
-
startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
|
|
152
|
-
endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
91
|
// src/pacing/au-duration.ts
|
|
158
92
|
function auDurationMsFromCodec(codec, sampleRate) {
|
|
159
93
|
if (!(sampleRate > 0)) throw new Error("sampleRate must be > 0");
|
|
@@ -174,7 +108,6 @@ function pictureDurationMs(framerate) {
|
|
|
174
108
|
}
|
|
175
109
|
export {
|
|
176
110
|
FrameCountPacer,
|
|
177
|
-
InterleaveTiming,
|
|
178
111
|
auDurationMsFromCodec,
|
|
179
112
|
pictureDurationMs
|
|
180
113
|
};
|
package/dist/pacing/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/pacing/frame-pacer.ts", "../../src/pacing/
|
|
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 * - the caller reports a standing backlog (see `observedDelayMs`)\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 * `observedDelayMs` is how far this frame is behind the producer's own clock,\n * measured by the caller against a producer-supplied sending instant (for\n * MMTP, the header send-wall timestamp). It is optional because the anchored\n * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST\n * frame read, so `aheadMs` measures consumption against the join instant and\n * not against the producer. Once `aheadMs` reaches the threshold the loop\n * consumes at exactly the source rate, and recovering latency present at join\n * requires consuming FASTER than the source rate. So without this argument a\n * consumer that joined a live stream mid-block keeps that join latency for the\n * life of the subscription.\n *\n * A caller that can measure the real distance from the producer passes it and\n * gets a drain: while the backlog exceeds the threshold there is nothing to\n * pace against, so return 0 and let the transport set the rate. At the live\n * edge the anchored-rate behavior below applies unchanged.\n *\n * Callers with no producer clock omit it and keep the pure ATSC3-style\n * anchored pacing (see the class doc), where a first-frame anchor is correct\n * because a broadcast push has no live edge to catch up to.\n */\n computeSleepMs(observedDelayMs?: number): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n // Only a finite measurement can license the drain. NaN/Infinity mean the\n // caller could not measure the distance, which is not the same claim as\n // \"the consumer is at the edge\", so fall through to anchored pacing.\n if (observedDelayMs !== undefined && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {\n return 0\n }\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 * This is the `k-padded-v1` K-stride pacing model and nothing more: it is\n * presentation-time arithmetic for a stream whose blocks are K symbols and D\n * media groups long, NOT an FEC block locator. FecBlockMapper no longer maps\n * media groups to blocks (BLO-35597), so the stride math lives here, and a\n * stream under any other `fec.blockProfile` (an `iso-ssbg1-v1` block's length\n * is stated per block on the wire, so it has no stride) is refused.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\n/** 2^32: SS_ID is a 32-bit serial number (ISO/IEC 23008-1 C.5.2). */\nconst SS_ID_SPAN = 0x1_0000_0000\n\nexport interface InterleaveTimingOpts {\n /** Track's FEC mapper (provides K and D); must be `k-padded-v1`. */\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 /** Source symbols contributed by each media group: ceil(K / D). */\n readonly symbolsPerGroup: 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 if (opts.mapper.blockProfile !== 'k-padded-v1') {\n throw new Error(\n `InterleaveTiming is the k-padded-v1 K-stride pacing model; a ${opts.mapper.blockProfile ?? 'non-FEC'} ` +\n 'stream has no K-stride block geometry',\n )\n }\n this.mapper = opts.mapper\n this.symbolsPerGroup = Math.ceil(opts.mapper.k / opts.mapper.d)\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 // \u00A78.3 under k-padded-v1: SBN = floor(G / D), ESI = groupInBlock \u00D7\n // symbolsPerGroup + O, SS_ID = SBN \u00D7 K + ESI (a 32-bit serial number;\n // reduce SBN first so the product stays exact).\n const { k, d } = this.mapper\n const sbn = Math.floor(groupId / d)\n const esi = (groupId - sbn * d) * this.symbolsPerGroup + objectId\n const ssId = ((sbn % SS_ID_SPAN) * k + esi) % SS_ID_SPAN\n return this.anchorMpuUs + 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 // k-padded-v1: SBN = floor(SS_ID / K), ESI = SS_ID mod K.\n const sbn = Math.floor(rawSsId / this.mapper.k)\n const esi = rawSsId % this.mapper.k\n // Invert \u00A78.3: esi = groupInBlock * symbolsPerGroup + objectId\n const spg = this.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": ";AAoDO,IAAM,kBAAN,MAAsB;AAAA,EAK3B,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,YAAY;AAAA,EAEZ,YAAY,MAA2B;AACrC,QAAI,EAAE,KAAK,kBAAkB,IAAI;AAC/B,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,SAAK,kBAAkB,KAAK;AAC5B,SAAK,cAAc,KAAK,OAAO;AAC/B,SAAK,eAAe,KAAK,IAAI,GAAG,KAAK,gBAAgB,CAAC;AAAA,EACxD;AAAA;AAAA,EAGA,cAAoB;AAClB,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,gBAAgB,IAAI;AACzB,WAAK,YAAY;AAAA,IACnB;AACA,SAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCA,eAAe,iBAAkC;AAC/C,QAAI,KAAK,eAAe,EAAG,QAAO;AAClC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,QAAI,KAAK,eAAe,KAAK,aAAc,QAAO;AAIlD,QAAI,oBAAoB,UAAa,OAAO,SAAS,eAAe,KAAK,kBAAkB,KAAK,aAAa;AAC3G,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,KAAK,cAAc,KAAK;AACxC,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAM,UAAU,UAAU;AAC1B,QAAI,WAAW,KAAK,YAAa,QAAO;AACxC,WAAO,UAAU,KAAK,cAAc;AAAA,EACtC;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,IAAI,WAA0B;AAC5B,UAAM,UAAU,KAAK,cAAc,KAAK;AACxC,UAAM,SAAS,KAAK,YAAY,IAAI,IAAI,KAAK,gBAAgB;AAC7D,WAAO;AAAA,MACL,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,MACA,SAAS,UAAU;AAAA,MACnB,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AACF;AAEA,SAAS,MAAc;AAGrB,MAAI,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,YAAY;AAC/E,WAAO,YAAY,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,IAAI;AAClB;;;
|
|
3
|
+
"sources": ["../../src/pacing/frame-pacer.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 * - the caller reports a standing backlog (see `observedDelayMs`)\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 * `observedDelayMs` is how far this frame is behind the producer's own clock,\n * measured by the caller against a producer-supplied sending instant (for\n * MMTP, the header send-wall timestamp). It is optional because the anchored\n * rate below is blind to it: `#anchorWallMs` is the wall time of the FIRST\n * frame read, so `aheadMs` measures consumption against the join instant and\n * not against the producer. Once `aheadMs` reaches the threshold the loop\n * consumes at exactly the source rate, and recovering latency present at join\n * requires consuming FASTER than the source rate. So without this argument a\n * consumer that joined a live stream mid-block keeps that join latency for the\n * life of the subscription.\n *\n * A caller that can measure the real distance from the producer passes it and\n * gets a drain: while the backlog exceeds the threshold there is nothing to\n * pace against, so return 0 and let the transport set the rate. At the live\n * edge the anchored-rate behavior below applies unchanged.\n *\n * Callers with no producer clock omit it and keep the pure ATSC3-style\n * anchored pacing (see the class doc), where a first-frame anchor is correct\n * because a broadcast push has no live edge to catch up to.\n */\n computeSleepMs(observedDelayMs?: number): number {\n if (this.thresholdMs <= 0) return 0\n if (!this.#anchored) return 0\n if (this.#frameCount <= this.warmupFrames) return 0\n // Only a finite measurement can license the drain. NaN/Infinity mean the\n // caller could not measure the distance, which is not the same claim as\n // \"the consumer is at the edge\", so fall through to anchored pacing.\n if (observedDelayMs !== undefined && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {\n return 0\n }\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 * 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": ";AAoDO,IAAM,kBAAN,MAAsB;AAAA,EAK3B,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,YAAY;AAAA,EAEZ,YAAY,MAA2B;AACrC,QAAI,EAAE,KAAK,kBAAkB,IAAI;AAC/B,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AACA,SAAK,kBAAkB,KAAK;AAC5B,SAAK,cAAc,KAAK,OAAO;AAC/B,SAAK,eAAe,KAAK,IAAI,GAAG,KAAK,gBAAgB,CAAC;AAAA,EACxD;AAAA;AAAA,EAGA,cAAoB;AAClB,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,gBAAgB,IAAI;AACzB,WAAK,YAAY;AAAA,IACnB;AACA,SAAK;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgCA,eAAe,iBAAkC;AAC/C,QAAI,KAAK,eAAe,EAAG,QAAO;AAClC,QAAI,CAAC,KAAK,UAAW,QAAO;AAC5B,QAAI,KAAK,eAAe,KAAK,aAAc,QAAO;AAIlD,QAAI,oBAAoB,UAAa,OAAO,SAAS,eAAe,KAAK,kBAAkB,KAAK,aAAa;AAC3G,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,KAAK,cAAc,KAAK;AACxC,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAM,UAAU,UAAU;AAC1B,QAAI,WAAW,KAAK,YAAa,QAAO;AACxC,WAAO,UAAU,KAAK,cAAc;AAAA,EACtC;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,cAAc;AACnB,SAAK,gBAAgB;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,IAAI,WAA0B;AAC5B,UAAM,UAAU,KAAK,cAAc,KAAK;AACxC,UAAM,SAAS,KAAK,YAAY,IAAI,IAAI,KAAK,gBAAgB;AAC7D,WAAO;AAAA,MACL,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,MACA,SAAS,UAAU;AAAA,MACnB,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AACF;AAEA,SAAS,MAAc;AAGrB,MAAI,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,YAAY;AAC/E,WAAO,YAAY,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,IAAI;AAClB;;;AC9IO,SAAS,sBAAsB,OAAe,YAA4B;AAC/E,MAAI,EAAE,aAAa,GAAI,OAAM,IAAI,MAAM,wBAAwB;AAC/D,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,MAAM,GAAG;AACxD,WAAQ,MAAM,aAAc;AAAA,EAC9B;AACA,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,KAAK,KAAK,MAAM,WAAW,KAAK,GAAG;AAClF,WAAQ,OAAO,aAAc;AAAA,EAC/B;AACA,MAAI,MAAM,WAAW,MAAM,KAAK,MAAM,WAAW,MAAM,GAAG;AACxD,WAAQ,OAAO,aAAc;AAAA,EAC/B;AACA,QAAM,IAAI,MAAM,mDAAmD,KAAK,EAAE;AAC5E;AAKO,SAAS,kBAAkB,WAA2B;AAC3D,MAAI,EAAE,YAAY,GAAI,OAAM,IAAI,MAAM,uBAAuB;AAC7D,SAAO,MAAO;AAChB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockcast/mmt-render",
|
|
3
|
-
"version": "0.3.0-main.
|
|
4
|
-
"description": "Shared MMT render pipeline (FEC-aware pacing,
|
|
3
|
+
"version": "0.3.0-main.45cce9806486",
|
|
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.3.0-main.
|
|
77
|
-
"@blockcast/mmt-fec": "0.3.0-main.
|
|
72
|
+
"@blockcast/mmt-container": "0.3.0-main.45cce9806486",
|
|
73
|
+
"@blockcast/mmt-fec": "0.3.0-main.45cce9806486"
|
|
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
|
|
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
|
package/src/pacing/index.ts
CHANGED
|
@@ -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,73 +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
|
-
* This is the `k-padded-v1` K-stride pacing model and nothing more: it is
|
|
23
|
-
* presentation-time arithmetic for a stream whose blocks are K symbols and D
|
|
24
|
-
* media groups long, NOT an FEC block locator. FecBlockMapper no longer maps
|
|
25
|
-
* media groups to blocks (BLO-35597), so the stride math lives here, and a
|
|
26
|
-
* stream under any other `fec.blockProfile` (an `iso-ssbg1-v1` block's length
|
|
27
|
-
* is stated per block on the wire, so it has no stride) is refused.
|
|
28
|
-
*/
|
|
29
|
-
import type { FecBlockMapper } from '@blockcast/mmt-fec';
|
|
30
|
-
export interface InterleaveTimingOpts {
|
|
31
|
-
/** Track's FEC mapper (provides K and D); must be `k-padded-v1`. */
|
|
32
|
-
mapper: FecBlockMapper;
|
|
33
|
-
/** Nominal AU duration in microseconds. */
|
|
34
|
-
frameDurationUs: number;
|
|
35
|
-
/** Wall-time anchor: the mpu_timestamp of the first observed AU (µs). */
|
|
36
|
-
anchorMpuUs: number;
|
|
37
|
-
}
|
|
38
|
-
export declare class InterleaveTiming {
|
|
39
|
-
readonly mapper: FecBlockMapper;
|
|
40
|
-
readonly frameDurationUs: number;
|
|
41
|
-
readonly anchorMpuUs: number;
|
|
42
|
-
/** Source symbols contributed by each media group: ceil(K / D). */
|
|
43
|
-
readonly symbolsPerGroup: number;
|
|
44
|
-
constructor(opts: InterleaveTimingOpts);
|
|
45
|
-
/**
|
|
46
|
-
* Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
|
|
47
|
-
* Per draft §8.3 + Sony anchor.
|
|
48
|
-
*/
|
|
49
|
-
presentationUs(groupId: number, objectId: number): number;
|
|
50
|
-
/**
|
|
51
|
-
* Presentation time (µs) for an AU identified by its flat SS_ID
|
|
52
|
-
* (multicast FEC Payload ID path).
|
|
53
|
-
*/
|
|
54
|
-
presentationUsFromSsId(ssId: number): number;
|
|
55
|
-
/**
|
|
56
|
-
* Inverse: which (group_id, object_id) AU is scheduled at the given
|
|
57
|
-
* wall offset (µs) from anchor? Clamps to ssId≥0.
|
|
58
|
-
*/
|
|
59
|
-
frameAtOffset(offsetUs: number): {
|
|
60
|
-
groupId: number;
|
|
61
|
-
objectId: number;
|
|
62
|
-
ssId: number;
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Block start/end in microseconds. Source blocks span `D × GOP_duration`
|
|
66
|
-
* of wall time; this returns the anchored wall range [startUs, endUs).
|
|
67
|
-
*/
|
|
68
|
-
blockTimeRangeUs(sbn: number): {
|
|
69
|
-
startUs: number;
|
|
70
|
-
endUs: number;
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAKxD,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,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;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;gBAEpB,IAAI,EAAE,oBAAoB;IAmBtC;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAWzD;;;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;IAapF;;;OAGG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;CAQlE"}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// src/pacing/interleave-timing.ts
|
|
2
|
-
var SS_ID_SPAN = 4294967296;
|
|
3
|
-
var InterleaveTiming = class {
|
|
4
|
-
constructor(opts) {
|
|
5
|
-
if (!(opts.frameDurationUs > 0)) {
|
|
6
|
-
throw new Error("frameDurationUs must be > 0");
|
|
7
|
-
}
|
|
8
|
-
if (!Number.isFinite(opts.anchorMpuUs)) {
|
|
9
|
-
throw new Error("anchorMpuUs must be finite");
|
|
10
|
-
}
|
|
11
|
-
if (opts.mapper.blockProfile !== "k-padded-v1") {
|
|
12
|
-
throw new Error(
|
|
13
|
-
`InterleaveTiming is the k-padded-v1 K-stride pacing model; a ${opts.mapper.blockProfile ?? "non-FEC"} stream has no K-stride block geometry`
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
this.mapper = opts.mapper;
|
|
17
|
-
this.symbolsPerGroup = Math.ceil(opts.mapper.k / opts.mapper.d);
|
|
18
|
-
this.frameDurationUs = opts.frameDurationUs;
|
|
19
|
-
this.anchorMpuUs = opts.anchorMpuUs;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
|
|
23
|
-
* Per draft §8.3 + Sony anchor.
|
|
24
|
-
*/
|
|
25
|
-
presentationUs(groupId, objectId) {
|
|
26
|
-
const { k, d } = this.mapper;
|
|
27
|
-
const sbn = Math.floor(groupId / d);
|
|
28
|
-
const esi = (groupId - sbn * d) * this.symbolsPerGroup + objectId;
|
|
29
|
-
const ssId = (sbn % SS_ID_SPAN * k + esi) % SS_ID_SPAN;
|
|
30
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Presentation time (µs) for an AU identified by its flat SS_ID
|
|
34
|
-
* (multicast FEC Payload ID path).
|
|
35
|
-
*/
|
|
36
|
-
presentationUsFromSsId(ssId) {
|
|
37
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Inverse: which (group_id, object_id) AU is scheduled at the given
|
|
41
|
-
* wall offset (µs) from anchor? Clamps to ssId≥0.
|
|
42
|
-
*/
|
|
43
|
-
frameAtOffset(offsetUs) {
|
|
44
|
-
const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs));
|
|
45
|
-
const sbn = Math.floor(rawSsId / this.mapper.k);
|
|
46
|
-
const esi = rawSsId % this.mapper.k;
|
|
47
|
-
const spg = this.symbolsPerGroup;
|
|
48
|
-
const groupInBlock = Math.floor(esi / spg);
|
|
49
|
-
const objectId = esi - groupInBlock * spg;
|
|
50
|
-
const groupId = sbn * this.mapper.d + groupInBlock;
|
|
51
|
-
return { groupId, objectId, ssId: rawSsId };
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Block start/end in microseconds. Source blocks span `D × GOP_duration`
|
|
55
|
-
* of wall time; this returns the anchored wall range [startUs, endUs).
|
|
56
|
-
*/
|
|
57
|
-
blockTimeRangeUs(sbn) {
|
|
58
|
-
const firstSsId = sbn * this.mapper.k;
|
|
59
|
-
const lastSsId = (sbn + 1) * this.mapper.k;
|
|
60
|
-
return {
|
|
61
|
-
startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
|
|
62
|
-
endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
export {
|
|
67
|
-
InterleaveTiming
|
|
68
|
-
};
|
|
69
|
-
//# 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 * This is the `k-padded-v1` K-stride pacing model and nothing more: it is\n * presentation-time arithmetic for a stream whose blocks are K symbols and D\n * media groups long, NOT an FEC block locator. FecBlockMapper no longer maps\n * media groups to blocks (BLO-35597), so the stride math lives here, and a\n * stream under any other `fec.blockProfile` (an `iso-ssbg1-v1` block's length\n * is stated per block on the wire, so it has no stride) is refused.\n */\n\nimport type { FecBlockMapper } from '@blockcast/mmt-fec'\n\n/** 2^32: SS_ID is a 32-bit serial number (ISO/IEC 23008-1 C.5.2). */\nconst SS_ID_SPAN = 0x1_0000_0000\n\nexport interface InterleaveTimingOpts {\n /** Track's FEC mapper (provides K and D); must be `k-padded-v1`. */\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 /** Source symbols contributed by each media group: ceil(K / D). */\n readonly symbolsPerGroup: 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 if (opts.mapper.blockProfile !== 'k-padded-v1') {\n throw new Error(\n `InterleaveTiming is the k-padded-v1 K-stride pacing model; a ${opts.mapper.blockProfile ?? 'non-FEC'} ` +\n 'stream has no K-stride block geometry',\n )\n }\n this.mapper = opts.mapper\n this.symbolsPerGroup = Math.ceil(opts.mapper.k / opts.mapper.d)\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 // \u00A78.3 under k-padded-v1: SBN = floor(G / D), ESI = groupInBlock \u00D7\n // symbolsPerGroup + O, SS_ID = SBN \u00D7 K + ESI (a 32-bit serial number;\n // reduce SBN first so the product stays exact).\n const { k, d } = this.mapper\n const sbn = Math.floor(groupId / d)\n const esi = (groupId - sbn * d) * this.symbolsPerGroup + objectId\n const ssId = ((sbn % SS_ID_SPAN) * k + esi) % SS_ID_SPAN\n return this.anchorMpuUs + 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 // k-padded-v1: SBN = floor(SS_ID / K), ESI = SS_ID mod K.\n const sbn = Math.floor(rawSsId / this.mapper.k)\n const esi = rawSsId % this.mapper.k\n // Invert \u00A78.3: esi = groupInBlock * symbolsPerGroup + objectId\n const spg = this.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": ";AAgCA,IAAM,aAAa;AAWZ,IAAM,mBAAN,MAAuB;AAAA,EAO5B,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,QAAI,KAAK,OAAO,iBAAiB,eAAe;AAC9C,YAAM,IAAI;AAAA,QACR,gEAAgE,KAAK,OAAO,gBAAgB,SAAS;AAAA,MAEvG;AAAA,IACF;AACA,SAAK,SAAS,KAAK;AACnB,SAAK,kBAAkB,KAAK,KAAK,KAAK,OAAO,IAAI,KAAK,OAAO,CAAC;AAC9D,SAAK,kBAAkB,KAAK;AAC5B,SAAK,cAAc,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,SAAiB,UAA0B;AAIxD,UAAM,EAAE,GAAG,EAAE,IAAI,KAAK;AACtB,UAAM,MAAM,KAAK,MAAM,UAAU,CAAC;AAClC,UAAM,OAAO,UAAU,MAAM,KAAK,KAAK,kBAAkB;AACzD,UAAM,QAAS,MAAM,aAAc,IAAI,OAAO;AAC9C,WAAO,KAAK,cAAc,OAAO,KAAK;AAAA,EACxC;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;AAEvE,UAAM,MAAM,KAAK,MAAM,UAAU,KAAK,OAAO,CAAC;AAC9C,UAAM,MAAM,UAAU,KAAK,OAAO;AAElC,UAAM,MAAM,KAAK;AACjB,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,122 +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
|
-
* This is the `k-padded-v1` K-stride pacing model and nothing more: it is
|
|
23
|
-
* presentation-time arithmetic for a stream whose blocks are K symbols and D
|
|
24
|
-
* media groups long, NOT an FEC block locator. FecBlockMapper no longer maps
|
|
25
|
-
* media groups to blocks (BLO-35597), so the stride math lives here, and a
|
|
26
|
-
* stream under any other `fec.blockProfile` (an `iso-ssbg1-v1` block's length
|
|
27
|
-
* is stated per block on the wire, so it has no stride) is refused.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import type { FecBlockMapper } from '@blockcast/mmt-fec'
|
|
31
|
-
|
|
32
|
-
/** 2^32: SS_ID is a 32-bit serial number (ISO/IEC 23008-1 C.5.2). */
|
|
33
|
-
const SS_ID_SPAN = 0x1_0000_0000
|
|
34
|
-
|
|
35
|
-
export interface InterleaveTimingOpts {
|
|
36
|
-
/** Track's FEC mapper (provides K and D); must be `k-padded-v1`. */
|
|
37
|
-
mapper: FecBlockMapper
|
|
38
|
-
/** Nominal AU duration in microseconds. */
|
|
39
|
-
frameDurationUs: number
|
|
40
|
-
/** Wall-time anchor: the mpu_timestamp of the first observed AU (µs). */
|
|
41
|
-
anchorMpuUs: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class InterleaveTiming {
|
|
45
|
-
readonly mapper: FecBlockMapper
|
|
46
|
-
readonly frameDurationUs: number
|
|
47
|
-
readonly anchorMpuUs: number
|
|
48
|
-
/** Source symbols contributed by each media group: ceil(K / D). */
|
|
49
|
-
readonly symbolsPerGroup: number
|
|
50
|
-
|
|
51
|
-
constructor(opts: InterleaveTimingOpts) {
|
|
52
|
-
if (!(opts.frameDurationUs > 0)) {
|
|
53
|
-
throw new Error('frameDurationUs must be > 0')
|
|
54
|
-
}
|
|
55
|
-
if (!Number.isFinite(opts.anchorMpuUs)) {
|
|
56
|
-
throw new Error('anchorMpuUs must be finite')
|
|
57
|
-
}
|
|
58
|
-
if (opts.mapper.blockProfile !== 'k-padded-v1') {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`InterleaveTiming is the k-padded-v1 K-stride pacing model; a ${opts.mapper.blockProfile ?? 'non-FEC'} ` +
|
|
61
|
-
'stream has no K-stride block geometry',
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
this.mapper = opts.mapper
|
|
65
|
-
this.symbolsPerGroup = Math.ceil(opts.mapper.k / opts.mapper.d)
|
|
66
|
-
this.frameDurationUs = opts.frameDurationUs
|
|
67
|
-
this.anchorMpuUs = opts.anchorMpuUs
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Presentation time (µs) for an AU at MoQ coords (group_id, object_id).
|
|
72
|
-
* Per draft §8.3 + Sony anchor.
|
|
73
|
-
*/
|
|
74
|
-
presentationUs(groupId: number, objectId: number): number {
|
|
75
|
-
// §8.3 under k-padded-v1: SBN = floor(G / D), ESI = groupInBlock ×
|
|
76
|
-
// symbolsPerGroup + O, SS_ID = SBN × K + ESI (a 32-bit serial number;
|
|
77
|
-
// reduce SBN first so the product stays exact).
|
|
78
|
-
const { k, d } = this.mapper
|
|
79
|
-
const sbn = Math.floor(groupId / d)
|
|
80
|
-
const esi = (groupId - sbn * d) * this.symbolsPerGroup + objectId
|
|
81
|
-
const ssId = ((sbn % SS_ID_SPAN) * k + esi) % SS_ID_SPAN
|
|
82
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Presentation time (µs) for an AU identified by its flat SS_ID
|
|
87
|
-
* (multicast FEC Payload ID path).
|
|
88
|
-
*/
|
|
89
|
-
presentationUsFromSsId(ssId: number): number {
|
|
90
|
-
return this.anchorMpuUs + ssId * this.frameDurationUs
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Inverse: which (group_id, object_id) AU is scheduled at the given
|
|
95
|
-
* wall offset (µs) from anchor? Clamps to ssId≥0.
|
|
96
|
-
*/
|
|
97
|
-
frameAtOffset(offsetUs: number): { groupId: number; objectId: number; ssId: number } {
|
|
98
|
-
const rawSsId = Math.max(0, Math.floor(offsetUs / this.frameDurationUs))
|
|
99
|
-
// k-padded-v1: SBN = floor(SS_ID / K), ESI = SS_ID mod K.
|
|
100
|
-
const sbn = Math.floor(rawSsId / this.mapper.k)
|
|
101
|
-
const esi = rawSsId % this.mapper.k
|
|
102
|
-
// Invert §8.3: esi = groupInBlock * symbolsPerGroup + objectId
|
|
103
|
-
const spg = this.symbolsPerGroup
|
|
104
|
-
const groupInBlock = Math.floor(esi / spg)
|
|
105
|
-
const objectId = esi - groupInBlock * spg
|
|
106
|
-
const groupId = sbn * this.mapper.d + groupInBlock
|
|
107
|
-
return { groupId, objectId, ssId: rawSsId }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Block start/end in microseconds. Source blocks span `D × GOP_duration`
|
|
112
|
-
* of wall time; this returns the anchored wall range [startUs, endUs).
|
|
113
|
-
*/
|
|
114
|
-
blockTimeRangeUs(sbn: number): { startUs: number; endUs: number } {
|
|
115
|
-
const firstSsId = sbn * this.mapper.k
|
|
116
|
-
const lastSsId = (sbn + 1) * this.mapper.k
|
|
117
|
-
return {
|
|
118
|
-
startUs: this.anchorMpuUs + firstSsId * this.frameDurationUs,
|
|
119
|
-
endUs: this.anchorMpuUs + lastSsId * this.frameDurationUs,
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|