@blockcast/mmt-render 0.1.0-main.f8328f38cfb6 → 0.1.0-main.fccb87c7444d
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 +5 -4
- package/dist/audio/audio-render-pipeline.js +192 -247
- package/dist/audio/audio-render-pipeline.js.map +1 -1
- package/dist/audio/audio-ring-buffer.js +93 -127
- package/dist/audio/audio-ring-buffer.js.map +1 -1
- package/dist/audio/index.js +288 -360
- package/dist/audio/index.js.map +2 -2
- package/dist/pacing/frame-pacer.js +18 -38
- package/dist/pacing/frame-pacer.js.map +1 -1
- package/dist/pacing/index.js +33 -42
- package/dist/pacing/index.js.map +2 -2
- package/dist/pacing/interleave-timing.d.ts +10 -1
- package/dist/pacing/interleave-timing.d.ts.map +1 -1
- package/dist/pacing/interleave-timing.js +15 -4
- package/dist/pacing/interleave-timing.js.map +2 -2
- package/dist/video/index.js +103 -137
- package/dist/video/index.js.map +1 -1
- package/dist/video/keyframe-gate.js +5 -17
- package/dist/video/keyframe-gate.js.map +1 -1
- package/dist/video/video-render-pipeline.js +103 -137
- package/dist/video/video-render-pipeline.js.map +1 -1
- package/dist/worklet/render-worklet.js +171 -219
- package/dist/worklet/render-worklet.js.map +2 -2
- package/package.json +3 -3
- package/src/pacing/interleave-timing.ts +32 -5
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Shared render pipeline for MMT players: FEC-aware wall-clock pacing, per-AU pres
|
|
|
8
8
|
pnpm add @blockcast/mmt-render
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Peer-installs through normal deps: `@blockcast/mmt-container` (provides `MmtpDecodePipeline` and the FEC-aware MFU reassembler) and `@blockcast/mmt-fec` (provides `FecBlockMapper`, which carries `pacingThresholdMs`, `K`, `D
|
|
11
|
+
Peer-installs through normal deps: `@blockcast/mmt-container` (provides `MmtpDecodePipeline` and the FEC-aware MFU reassembler) and `@blockcast/mmt-fec` (provides `FecBlockMapper`, which carries `pacingThresholdMs`, `K`, `D` and the catalog's required `fec.blockProfile`).
|
|
12
12
|
|
|
13
13
|
## Quick start (audio)
|
|
14
14
|
|
|
@@ -17,7 +17,8 @@ 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
|
-
|
|
20
|
+
// blockProfile is the catalog `fec.blockProfile`: required, never defaulted.
|
|
21
|
+
const mapper = new FecBlockMapper({ blockProfile: "k-padded-v1", interleaveDepthMs: 66, gopDurationMs: 21.33, k: 32 })
|
|
21
22
|
|
|
22
23
|
const audio = new AudioRenderPipeline({
|
|
23
24
|
context: ctx,
|
|
@@ -103,7 +104,7 @@ Media-agnostic wall-clock pacer. Anchors on the first `notifyFrame()` call; comp
|
|
|
103
104
|
|
|
104
105
|
### `InterleaveTiming` (`src/pacing/interleave-timing.ts`)
|
|
105
106
|
|
|
106
|
-
Per-AU presentation-time reconstruction from FEC coordinates
|
|
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`:
|
|
107
108
|
|
|
108
109
|
```
|
|
109
110
|
au_index_in_block = groupInBlock × symbolsPerGroup + objectId
|
|
@@ -189,7 +190,7 @@ There is **no CMCD `x-bc-*` emission, no catalog (CMSF) inspector, and no MPU/MF
|
|
|
189
190
|
## Related packages
|
|
190
191
|
|
|
191
192
|
- `@blockcast/mmt-container` — `MmtpDecodePipeline` (consumed for AU framing), MFU reassembly, catalog parser
|
|
192
|
-
- `@blockcast/mmt-fec` — `FecBlockMapper` (provides `pacingThresholdMs`, `K`, `D`, `
|
|
193
|
+
- `@blockcast/mmt-fec` — `FecBlockMapper` (provides `pacingThresholdMs`, `K`, `D`, `blockProfile`); `NullBlockMapper` disables pacing
|
|
193
194
|
- `@blockcast/mmt-transport` — produces the MoQ objects that feed `feedMoqObject()`
|
|
194
195
|
|
|
195
196
|
## Testing
|
|
@@ -1,30 +1,12 @@
|
|
|
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
1
|
// src/audio/audio-render-pipeline.ts
|
|
19
2
|
import { MmtpDecodePipeline } from "@blockcast/mmt-container/mmtp-decode-pipeline";
|
|
20
3
|
|
|
21
4
|
// src/pacing/frame-pacer.ts
|
|
22
|
-
var _frameCount, _anchorWallMs, _anchored;
|
|
23
5
|
var FrameCountPacer = class {
|
|
6
|
+
#frameCount = 0;
|
|
7
|
+
#anchorWallMs = 0;
|
|
8
|
+
#anchored = false;
|
|
24
9
|
constructor(opts) {
|
|
25
|
-
__privateAdd(this, _frameCount, 0);
|
|
26
|
-
__privateAdd(this, _anchorWallMs, 0);
|
|
27
|
-
__privateAdd(this, _anchored, false);
|
|
28
10
|
if (!(opts.frameDurationMs > 0)) {
|
|
29
11
|
throw new Error("frameDurationMs must be > 0");
|
|
30
12
|
}
|
|
@@ -34,11 +16,11 @@ var FrameCountPacer = class {
|
|
|
34
16
|
}
|
|
35
17
|
/** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
|
|
36
18
|
notifyFrame() {
|
|
37
|
-
if (!
|
|
38
|
-
|
|
39
|
-
|
|
19
|
+
if (!this.#anchored) {
|
|
20
|
+
this.#anchorWallMs = now();
|
|
21
|
+
this.#anchored = true;
|
|
40
22
|
}
|
|
41
|
-
|
|
23
|
+
this.#frameCount++;
|
|
42
24
|
}
|
|
43
25
|
/**
|
|
44
26
|
* Compute sleep ms before reading the next MoQ object.
|
|
@@ -72,39 +54,36 @@ var FrameCountPacer = class {
|
|
|
72
54
|
*/
|
|
73
55
|
computeSleepMs(observedDelayMs) {
|
|
74
56
|
if (this.thresholdMs <= 0) return 0;
|
|
75
|
-
if (!
|
|
76
|
-
if (
|
|
57
|
+
if (!this.#anchored) return 0;
|
|
58
|
+
if (this.#frameCount <= this.warmupFrames) return 0;
|
|
77
59
|
if (observedDelayMs !== void 0 && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {
|
|
78
60
|
return 0;
|
|
79
61
|
}
|
|
80
|
-
const mediaMs =
|
|
81
|
-
const wallMs = now() -
|
|
62
|
+
const mediaMs = this.#frameCount * this.frameDurationMs;
|
|
63
|
+
const wallMs = now() - this.#anchorWallMs;
|
|
82
64
|
const aheadMs = mediaMs - wallMs;
|
|
83
65
|
if (aheadMs <= this.thresholdMs) return 0;
|
|
84
66
|
return aheadMs - this.thresholdMs / 2;
|
|
85
67
|
}
|
|
86
68
|
/** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
|
|
87
69
|
reset() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
this.#frameCount = 0;
|
|
71
|
+
this.#anchorWallMs = 0;
|
|
72
|
+
this.#anchored = false;
|
|
91
73
|
}
|
|
92
74
|
get snapshot() {
|
|
93
|
-
const mediaMs =
|
|
94
|
-
const wallMs =
|
|
75
|
+
const mediaMs = this.#frameCount * this.frameDurationMs;
|
|
76
|
+
const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0;
|
|
95
77
|
return {
|
|
96
|
-
frameCount:
|
|
78
|
+
frameCount: this.#frameCount,
|
|
97
79
|
mediaMs,
|
|
98
80
|
wallMs,
|
|
99
81
|
aheadMs: mediaMs - wallMs,
|
|
100
82
|
thresholdMs: this.thresholdMs,
|
|
101
|
-
anchored:
|
|
83
|
+
anchored: this.#anchored
|
|
102
84
|
};
|
|
103
85
|
}
|
|
104
86
|
};
|
|
105
|
-
_frameCount = new WeakMap();
|
|
106
|
-
_anchorWallMs = new WeakMap();
|
|
107
|
-
_anchored = new WeakMap();
|
|
108
87
|
function now() {
|
|
109
88
|
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
110
89
|
return performance.now();
|
|
@@ -135,260 +114,226 @@ function workletModuleUrl() {
|
|
|
135
114
|
var WORKLET_PROCESSOR_NAME = "mmt-render";
|
|
136
115
|
|
|
137
116
|
// src/audio/audio-render-pipeline.ts
|
|
138
|
-
var _context, _pacer, _mapper, _codec, _packetId, _sampleRate, _channels, _description, _latencyMs, _startupFrames, _startupThresholdFrames, _capacityFrames, _diag, _warmupFrames, _onState, _onDiag, _onError, _reassemblyPolicy, _decoder, _pipeline, _worklet, _fecClient, _closed, _readyPromise, _workletGeneration, _startupReleased, _AudioRenderPipeline_instances, initialize_fn, onDecoded_fn;
|
|
139
117
|
var AudioRenderPipeline = class {
|
|
118
|
+
#context;
|
|
119
|
+
#pacer;
|
|
120
|
+
#mapper;
|
|
121
|
+
#codec;
|
|
122
|
+
#packetId;
|
|
123
|
+
#sampleRate;
|
|
124
|
+
#channels;
|
|
125
|
+
#description;
|
|
126
|
+
#latencyMs;
|
|
127
|
+
#startupFrames;
|
|
128
|
+
#startupThresholdFrames;
|
|
129
|
+
#capacityFrames;
|
|
130
|
+
#diag;
|
|
131
|
+
#warmupFrames;
|
|
132
|
+
#onState;
|
|
133
|
+
#onDiag;
|
|
134
|
+
#onError;
|
|
135
|
+
#reassemblyPolicy;
|
|
136
|
+
#decoder = null;
|
|
137
|
+
#pipeline = null;
|
|
138
|
+
#worklet = null;
|
|
139
|
+
#fecClient;
|
|
140
|
+
#closed = false;
|
|
141
|
+
#readyPromise = null;
|
|
142
|
+
#workletGeneration = 0;
|
|
143
|
+
#startupReleased = false;
|
|
140
144
|
constructor(opts) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
__privateAdd(this, _reassemblyPolicy);
|
|
160
|
-
__privateAdd(this, _decoder, null);
|
|
161
|
-
__privateAdd(this, _pipeline, null);
|
|
162
|
-
__privateAdd(this, _worklet, null);
|
|
163
|
-
__privateAdd(this, _fecClient);
|
|
164
|
-
__privateAdd(this, _closed, false);
|
|
165
|
-
__privateAdd(this, _readyPromise, null);
|
|
166
|
-
__privateAdd(this, _workletGeneration, 0);
|
|
167
|
-
__privateAdd(this, _startupReleased, false);
|
|
168
|
-
__privateSet(this, _context, opts.context);
|
|
169
|
-
__privateSet(this, _mapper, opts.mapper);
|
|
170
|
-
__privateSet(this, _codec, opts.codec);
|
|
171
|
-
__privateSet(this, _packetId, opts.packetId);
|
|
172
|
-
__privateSet(this, _sampleRate, opts.sampleRate);
|
|
173
|
-
__privateSet(this, _channels, opts.channels);
|
|
174
|
-
__privateSet(this, _description, opts.description);
|
|
175
|
-
__privateSet(this, _latencyMs, opts.latencyMs);
|
|
176
|
-
__privateSet(this, _startupFrames, opts.startupFrames);
|
|
177
|
-
__privateSet(this, _startupThresholdFrames, opts.startupFrames ?? Math.ceil(opts.sampleRate * (opts.latencyMs / 1e3)));
|
|
178
|
-
__privateSet(this, _capacityFrames, opts.capacityFrames);
|
|
179
|
-
__privateSet(this, _diag, opts.diag ?? false);
|
|
180
|
-
__privateSet(this, _warmupFrames, opts.warmupFrames ?? 0);
|
|
181
|
-
__privateSet(this, _onState, opts.onState);
|
|
182
|
-
__privateSet(this, _onDiag, opts.onDiag);
|
|
183
|
-
__privateSet(this, _onError, opts.onError);
|
|
184
|
-
__privateSet(this, _fecClient, opts.fecClient);
|
|
185
|
-
__privateSet(this, _reassemblyPolicy, opts.reassemblyPolicy);
|
|
145
|
+
this.#context = opts.context;
|
|
146
|
+
this.#mapper = opts.mapper;
|
|
147
|
+
this.#codec = opts.codec;
|
|
148
|
+
this.#packetId = opts.packetId;
|
|
149
|
+
this.#sampleRate = opts.sampleRate;
|
|
150
|
+
this.#channels = opts.channels;
|
|
151
|
+
this.#description = opts.description;
|
|
152
|
+
this.#latencyMs = opts.latencyMs;
|
|
153
|
+
this.#startupFrames = opts.startupFrames;
|
|
154
|
+
this.#startupThresholdFrames = opts.startupFrames ?? Math.ceil(opts.sampleRate * (opts.latencyMs / 1e3));
|
|
155
|
+
this.#capacityFrames = opts.capacityFrames;
|
|
156
|
+
this.#diag = opts.diag ?? false;
|
|
157
|
+
this.#warmupFrames = opts.warmupFrames ?? 0;
|
|
158
|
+
this.#onState = opts.onState;
|
|
159
|
+
this.#onDiag = opts.onDiag;
|
|
160
|
+
this.#onError = opts.onError;
|
|
161
|
+
this.#fecClient = opts.fecClient;
|
|
162
|
+
this.#reassemblyPolicy = opts.reassemblyPolicy;
|
|
186
163
|
const auDurationMs = auDurationMsFromCodec(opts.codec, opts.sampleRate);
|
|
187
|
-
|
|
164
|
+
this.#pacer = new FrameCountPacer({
|
|
188
165
|
frameDurationMs: auDurationMs,
|
|
189
166
|
mapper: opts.mapper,
|
|
190
|
-
warmupFrames:
|
|
191
|
-
})
|
|
167
|
+
warmupFrames: this.#warmupFrames
|
|
168
|
+
});
|
|
192
169
|
}
|
|
193
170
|
/** Load worklet module + create decoder + pipeline. Call once. */
|
|
194
171
|
async ready() {
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
return
|
|
172
|
+
if (this.#readyPromise) return this.#readyPromise;
|
|
173
|
+
this.#readyPromise = this.#initialize();
|
|
174
|
+
return this.#readyPromise;
|
|
175
|
+
}
|
|
176
|
+
async #initialize() {
|
|
177
|
+
await this.#context.audioWorklet.addModule(workletModuleUrl());
|
|
178
|
+
const worklet = new AudioWorkletNode(this.#context, WORKLET_PROCESSOR_NAME, {
|
|
179
|
+
numberOfInputs: 0,
|
|
180
|
+
numberOfOutputs: 1,
|
|
181
|
+
outputChannelCount: [this.#channels]
|
|
182
|
+
});
|
|
183
|
+
const initMsg = {
|
|
184
|
+
type: "init",
|
|
185
|
+
generation: ++this.#workletGeneration,
|
|
186
|
+
channels: this.#channels,
|
|
187
|
+
rate: this.#sampleRate,
|
|
188
|
+
latencyMs: this.#latencyMs,
|
|
189
|
+
startupFrames: this.#startupFrames,
|
|
190
|
+
capacityFrames: this.#capacityFrames,
|
|
191
|
+
holdUntilStart: true,
|
|
192
|
+
diag: this.#diag
|
|
193
|
+
};
|
|
194
|
+
worklet.port.postMessage(initMsg);
|
|
195
|
+
worklet.port.onmessage = (ev) => {
|
|
196
|
+
const m = ev.data;
|
|
197
|
+
if (m.type === "state") {
|
|
198
|
+
if (m.generation !== this.#workletGeneration) return;
|
|
199
|
+
this.#onState?.({
|
|
200
|
+
timestampUs: m.timestamp,
|
|
201
|
+
stalled: m.stalled,
|
|
202
|
+
bufferedFrames: m.bufferedFrames,
|
|
203
|
+
writtenFrames: m.writtenFrames
|
|
204
|
+
});
|
|
205
|
+
} else if (m.type === "ringBufDiag") {
|
|
206
|
+
if (m.generation !== this.#workletGeneration) return;
|
|
207
|
+
this.#onDiag?.({
|
|
208
|
+
reads: m.reads,
|
|
209
|
+
avgFill: m.avgFill,
|
|
210
|
+
capacity: m.capacity,
|
|
211
|
+
skipsPerSec: m.skipsPerSec,
|
|
212
|
+
dupsPerSec: m.dupsPerSec,
|
|
213
|
+
overflowSamplesPerSec: m.overflowSamplesPerSec,
|
|
214
|
+
underflowSamplesPerSec: m.underflowSamplesPerSec
|
|
215
|
+
});
|
|
216
|
+
} else if (m.type === "writeAck") {
|
|
217
|
+
if (m.generation !== this.#workletGeneration || this.#startupReleased || m.bufferedFrames < this.#startupThresholdFrames) return;
|
|
218
|
+
this.#startupReleased = true;
|
|
219
|
+
const start = {
|
|
220
|
+
type: "start",
|
|
221
|
+
generation: this.#workletGeneration
|
|
222
|
+
};
|
|
223
|
+
worklet.port.postMessage(start);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
this.#worklet = worklet;
|
|
227
|
+
const decoder = new AudioDecoder({
|
|
228
|
+
output: (data) => this.#onDecoded(data),
|
|
229
|
+
error: (err) => this.#onError?.(err)
|
|
230
|
+
});
|
|
231
|
+
const config = {
|
|
232
|
+
codec: this.#codec,
|
|
233
|
+
sampleRate: this.#sampleRate,
|
|
234
|
+
numberOfChannels: this.#channels,
|
|
235
|
+
...this.#description ? { description: this.#description } : {}
|
|
236
|
+
};
|
|
237
|
+
decoder.configure(config);
|
|
238
|
+
this.#decoder = decoder;
|
|
239
|
+
const pipeline = new MmtpDecodePipeline();
|
|
240
|
+
pipeline.configure({
|
|
241
|
+
codec: this.#codec,
|
|
242
|
+
packetId: this.#packetId,
|
|
243
|
+
description: this.#description,
|
|
244
|
+
reorderWindow: 0,
|
|
245
|
+
// audio doesn't need reorder — AAC/Opus frames are self-contained
|
|
246
|
+
reassemblyPolicy: this.#reassemblyPolicy
|
|
247
|
+
});
|
|
248
|
+
if (this.#fecClient) pipeline.setFecClient(this.#fecClient);
|
|
249
|
+
pipeline.onFrame((codecData, meta) => {
|
|
250
|
+
if (!this.#decoder || this.#decoder.state !== "configured") return;
|
|
251
|
+
if (codecData.byteLength === 0) return;
|
|
252
|
+
this.#pacer.notifyFrame();
|
|
253
|
+
try {
|
|
254
|
+
this.#decoder.decode(new EncodedAudioChunk({
|
|
255
|
+
type: "key",
|
|
256
|
+
data: codecData,
|
|
257
|
+
timestamp: meta.timestamp
|
|
258
|
+
}));
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this.#onError?.(err);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
this.#pipeline = pipeline;
|
|
198
264
|
}
|
|
199
265
|
/** Feed one MoQ object (raw MMTP packet). */
|
|
200
266
|
feedMoqObject(packet) {
|
|
201
|
-
|
|
267
|
+
this.#pipeline?.feedMoqObject(packet);
|
|
202
268
|
}
|
|
203
269
|
/** ms to sleep before the next object read. 0 when not anchored / not ahead. */
|
|
204
270
|
paceSleepMs() {
|
|
205
|
-
return
|
|
271
|
+
return this.#pacer.computeSleepMs();
|
|
206
272
|
}
|
|
207
273
|
/** Pacer snapshot (for diagnostics / UI). */
|
|
208
274
|
get pacerSnapshot() {
|
|
209
|
-
return
|
|
275
|
+
return this.#pacer.snapshot;
|
|
210
276
|
}
|
|
211
277
|
/** Reset pacer on source switch (MoQ ↔ multicast). */
|
|
212
278
|
resetPacer() {
|
|
213
|
-
|
|
279
|
+
this.#pacer.reset();
|
|
214
280
|
}
|
|
215
281
|
/** Drop queued PCM and its timestamp epoch at an explicit source boundary. */
|
|
216
282
|
resetPlayout(startUnstalled = false) {
|
|
217
|
-
|
|
218
|
-
|
|
283
|
+
this.#pacer.reset();
|
|
284
|
+
this.#startupReleased = true;
|
|
219
285
|
const message = {
|
|
220
286
|
type: "reset",
|
|
221
|
-
generation: ++
|
|
287
|
+
generation: ++this.#workletGeneration,
|
|
222
288
|
startUnstalled
|
|
223
289
|
};
|
|
224
|
-
|
|
290
|
+
this.#worklet?.port.postMessage(message);
|
|
225
291
|
}
|
|
226
292
|
/** Node to connect to destination / gain. */
|
|
227
293
|
get rootNode() {
|
|
228
|
-
if (!
|
|
229
|
-
return
|
|
294
|
+
if (!this.#worklet) throw new Error("AudioRenderPipeline.ready() must resolve first");
|
|
295
|
+
return this.#worklet;
|
|
230
296
|
}
|
|
231
297
|
close() {
|
|
232
|
-
if (
|
|
233
|
-
|
|
298
|
+
if (this.#closed) return;
|
|
299
|
+
this.#closed = true;
|
|
234
300
|
try {
|
|
235
|
-
|
|
301
|
+
this.#decoder?.close();
|
|
236
302
|
} catch {
|
|
237
303
|
}
|
|
238
304
|
try {
|
|
239
|
-
|
|
305
|
+
this.#pipeline?.dispose();
|
|
240
306
|
} catch {
|
|
241
307
|
}
|
|
242
308
|
try {
|
|
243
|
-
|
|
309
|
+
this.#worklet?.disconnect();
|
|
244
310
|
} catch {
|
|
245
311
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
312
|
+
this.#decoder = null;
|
|
313
|
+
this.#pipeline = null;
|
|
314
|
+
this.#worklet = null;
|
|
249
315
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
_packetId = new WeakMap();
|
|
256
|
-
_sampleRate = new WeakMap();
|
|
257
|
-
_channels = new WeakMap();
|
|
258
|
-
_description = new WeakMap();
|
|
259
|
-
_latencyMs = new WeakMap();
|
|
260
|
-
_startupFrames = new WeakMap();
|
|
261
|
-
_startupThresholdFrames = new WeakMap();
|
|
262
|
-
_capacityFrames = new WeakMap();
|
|
263
|
-
_diag = new WeakMap();
|
|
264
|
-
_warmupFrames = new WeakMap();
|
|
265
|
-
_onState = new WeakMap();
|
|
266
|
-
_onDiag = new WeakMap();
|
|
267
|
-
_onError = new WeakMap();
|
|
268
|
-
_reassemblyPolicy = new WeakMap();
|
|
269
|
-
_decoder = new WeakMap();
|
|
270
|
-
_pipeline = new WeakMap();
|
|
271
|
-
_worklet = new WeakMap();
|
|
272
|
-
_fecClient = new WeakMap();
|
|
273
|
-
_closed = new WeakMap();
|
|
274
|
-
_readyPromise = new WeakMap();
|
|
275
|
-
_workletGeneration = new WeakMap();
|
|
276
|
-
_startupReleased = new WeakMap();
|
|
277
|
-
_AudioRenderPipeline_instances = new WeakSet();
|
|
278
|
-
initialize_fn = async function() {
|
|
279
|
-
await __privateGet(this, _context).audioWorklet.addModule(workletModuleUrl());
|
|
280
|
-
const worklet = new AudioWorkletNode(__privateGet(this, _context), WORKLET_PROCESSOR_NAME, {
|
|
281
|
-
numberOfInputs: 0,
|
|
282
|
-
numberOfOutputs: 1,
|
|
283
|
-
outputChannelCount: [__privateGet(this, _channels)]
|
|
284
|
-
});
|
|
285
|
-
const initMsg = {
|
|
286
|
-
type: "init",
|
|
287
|
-
generation: ++__privateWrapper(this, _workletGeneration)._,
|
|
288
|
-
channels: __privateGet(this, _channels),
|
|
289
|
-
rate: __privateGet(this, _sampleRate),
|
|
290
|
-
latencyMs: __privateGet(this, _latencyMs),
|
|
291
|
-
startupFrames: __privateGet(this, _startupFrames),
|
|
292
|
-
capacityFrames: __privateGet(this, _capacityFrames),
|
|
293
|
-
holdUntilStart: true,
|
|
294
|
-
diag: __privateGet(this, _diag)
|
|
295
|
-
};
|
|
296
|
-
worklet.port.postMessage(initMsg);
|
|
297
|
-
worklet.port.onmessage = (ev) => {
|
|
298
|
-
var _a, _b;
|
|
299
|
-
const m = ev.data;
|
|
300
|
-
if (m.type === "state") {
|
|
301
|
-
if (m.generation !== __privateGet(this, _workletGeneration)) return;
|
|
302
|
-
(_a = __privateGet(this, _onState)) == null ? void 0 : _a.call(this, {
|
|
303
|
-
timestampUs: m.timestamp,
|
|
304
|
-
stalled: m.stalled,
|
|
305
|
-
bufferedFrames: m.bufferedFrames,
|
|
306
|
-
writtenFrames: m.writtenFrames
|
|
307
|
-
});
|
|
308
|
-
} else if (m.type === "ringBufDiag") {
|
|
309
|
-
if (m.generation !== __privateGet(this, _workletGeneration)) return;
|
|
310
|
-
(_b = __privateGet(this, _onDiag)) == null ? void 0 : _b.call(this, {
|
|
311
|
-
reads: m.reads,
|
|
312
|
-
avgFill: m.avgFill,
|
|
313
|
-
capacity: m.capacity,
|
|
314
|
-
skipsPerSec: m.skipsPerSec,
|
|
315
|
-
dupsPerSec: m.dupsPerSec,
|
|
316
|
-
overflowSamplesPerSec: m.overflowSamplesPerSec,
|
|
317
|
-
underflowSamplesPerSec: m.underflowSamplesPerSec
|
|
318
|
-
});
|
|
319
|
-
} else if (m.type === "writeAck") {
|
|
320
|
-
if (m.generation !== __privateGet(this, _workletGeneration) || __privateGet(this, _startupReleased) || m.bufferedFrames < __privateGet(this, _startupThresholdFrames)) return;
|
|
321
|
-
__privateSet(this, _startupReleased, true);
|
|
322
|
-
const start = {
|
|
323
|
-
type: "start",
|
|
324
|
-
generation: __privateGet(this, _workletGeneration)
|
|
325
|
-
};
|
|
326
|
-
worklet.port.postMessage(start);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
__privateSet(this, _worklet, worklet);
|
|
330
|
-
const decoder = new AudioDecoder({
|
|
331
|
-
output: (data) => __privateMethod(this, _AudioRenderPipeline_instances, onDecoded_fn).call(this, data),
|
|
332
|
-
error: (err) => {
|
|
333
|
-
var _a;
|
|
334
|
-
return (_a = __privateGet(this, _onError)) == null ? void 0 : _a.call(this, err);
|
|
316
|
+
// ─── Private ─────────────────────────────────────────────────────
|
|
317
|
+
#onDecoded(sample) {
|
|
318
|
+
if (this.#closed || !this.#worklet) {
|
|
319
|
+
sample.close();
|
|
320
|
+
return;
|
|
335
321
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
};
|
|
343
|
-
decoder.configure(config);
|
|
344
|
-
__privateSet(this, _decoder, decoder);
|
|
345
|
-
const pipeline = new MmtpDecodePipeline();
|
|
346
|
-
pipeline.configure({
|
|
347
|
-
codec: __privateGet(this, _codec),
|
|
348
|
-
packetId: __privateGet(this, _packetId),
|
|
349
|
-
description: __privateGet(this, _description),
|
|
350
|
-
reorderWindow: 0,
|
|
351
|
-
// audio doesn't need reorder — AAC/Opus frames are self-contained
|
|
352
|
-
reassemblyPolicy: __privateGet(this, _reassemblyPolicy)
|
|
353
|
-
});
|
|
354
|
-
if (__privateGet(this, _fecClient)) pipeline.setFecClient(__privateGet(this, _fecClient));
|
|
355
|
-
pipeline.onFrame((codecData, meta) => {
|
|
356
|
-
var _a;
|
|
357
|
-
if (!__privateGet(this, _decoder) || __privateGet(this, _decoder).state !== "configured") return;
|
|
358
|
-
if (codecData.byteLength === 0) return;
|
|
359
|
-
__privateGet(this, _pacer).notifyFrame();
|
|
360
|
-
try {
|
|
361
|
-
__privateGet(this, _decoder).decode(new EncodedAudioChunk({
|
|
362
|
-
type: "key",
|
|
363
|
-
data: codecData,
|
|
364
|
-
timestamp: meta.timestamp
|
|
365
|
-
}));
|
|
366
|
-
} catch (err) {
|
|
367
|
-
(_a = __privateGet(this, _onError)) == null ? void 0 : _a.call(this, err);
|
|
322
|
+
const timestamp = sample.timestamp;
|
|
323
|
+
const channelData = [];
|
|
324
|
+
for (let ch = 0; ch < sample.numberOfChannels; ch++) {
|
|
325
|
+
const data = new Float32Array(sample.numberOfFrames);
|
|
326
|
+
sample.copyTo(data, { format: "f32-planar", planeIndex: ch });
|
|
327
|
+
channelData.push(data);
|
|
368
328
|
}
|
|
369
|
-
});
|
|
370
|
-
__privateSet(this, _pipeline, pipeline);
|
|
371
|
-
};
|
|
372
|
-
// ─── Private ─────────────────────────────────────────────────────
|
|
373
|
-
onDecoded_fn = function(sample) {
|
|
374
|
-
if (__privateGet(this, _closed) || !__privateGet(this, _worklet)) {
|
|
375
329
|
sample.close();
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
sample.copyTo(data, { format: "f32-planar", planeIndex: ch });
|
|
383
|
-
channelData.push(data);
|
|
330
|
+
const msg = {
|
|
331
|
+
type: "data",
|
|
332
|
+
data: channelData,
|
|
333
|
+
timestamp
|
|
334
|
+
};
|
|
335
|
+
this.#worklet.port.postMessage(msg, channelData.map((d) => d.buffer));
|
|
384
336
|
}
|
|
385
|
-
sample.close();
|
|
386
|
-
const msg = {
|
|
387
|
-
type: "data",
|
|
388
|
-
data: channelData,
|
|
389
|
-
timestamp
|
|
390
|
-
};
|
|
391
|
-
__privateGet(this, _worklet).port.postMessage(msg, channelData.map((d) => d.buffer));
|
|
392
337
|
};
|
|
393
338
|
export {
|
|
394
339
|
AudioRenderPipeline
|