@blockcast/mmt-render 0.1.0-main.c71423b8a4e4 → 0.1.0-main.ce3c8aeb7f68
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/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 +18 -38
- package/dist/pacing/index.js.map +1 -1
- 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/dist/audio/index.js
CHANGED
|
@@ -1,39 +1,20 @@
|
|
|
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-ring-buffer.ts
|
|
19
|
-
var _buffer, _writeIndex, _readIndex, _stalled, _startupFrames, _resumeFrames, _resumeFollowsStartup, _hasStarted, _capacityHeadroomFrames, _timestampAnchors, _diagEnabled, _diagReads, _diagFillSum, _diagOverflowSamples, _AudioRingBuffer_instances, pruneTimestampAnchors_fn, refillFrames_fn;
|
|
20
2
|
var AudioRingBuffer = class {
|
|
3
|
+
#buffer;
|
|
4
|
+
#writeIndex = 0;
|
|
5
|
+
#readIndex = 0;
|
|
6
|
+
#stalled;
|
|
7
|
+
#startupFrames;
|
|
8
|
+
#resumeFrames;
|
|
9
|
+
#resumeFollowsStartup;
|
|
10
|
+
#hasStarted;
|
|
11
|
+
#capacityHeadroomFrames;
|
|
12
|
+
#timestampAnchors = [];
|
|
13
|
+
#diagEnabled;
|
|
14
|
+
#diagReads = 0;
|
|
15
|
+
#diagFillSum = 0;
|
|
16
|
+
#diagOverflowSamples = 0;
|
|
21
17
|
constructor(config) {
|
|
22
|
-
__privateAdd(this, _AudioRingBuffer_instances);
|
|
23
|
-
__privateAdd(this, _buffer);
|
|
24
|
-
__privateAdd(this, _writeIndex, 0);
|
|
25
|
-
__privateAdd(this, _readIndex, 0);
|
|
26
|
-
__privateAdd(this, _stalled);
|
|
27
|
-
__privateAdd(this, _startupFrames);
|
|
28
|
-
__privateAdd(this, _resumeFrames);
|
|
29
|
-
__privateAdd(this, _resumeFollowsStartup);
|
|
30
|
-
__privateAdd(this, _hasStarted);
|
|
31
|
-
__privateAdd(this, _capacityHeadroomFrames);
|
|
32
|
-
__privateAdd(this, _timestampAnchors, []);
|
|
33
|
-
__privateAdd(this, _diagEnabled);
|
|
34
|
-
__privateAdd(this, _diagReads, 0);
|
|
35
|
-
__privateAdd(this, _diagFillSum, 0);
|
|
36
|
-
__privateAdd(this, _diagOverflowSamples, 0);
|
|
37
18
|
if (!Number.isInteger(config.channels) || config.channels <= 0) {
|
|
38
19
|
throw new Error("invalid channels");
|
|
39
20
|
}
|
|
@@ -59,35 +40,35 @@ var AudioRingBuffer = class {
|
|
|
59
40
|
}
|
|
60
41
|
this.channels = config.channels;
|
|
61
42
|
this.rate = config.rate;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
this.#startupFrames = startupFrames;
|
|
44
|
+
this.#resumeFrames = resumeFrames;
|
|
45
|
+
this.#resumeFollowsStartup = config.resumeFrames === void 0;
|
|
46
|
+
this.#hasStarted = config.startUnstalled ?? false;
|
|
47
|
+
this.#capacityHeadroomFrames = capacityFrames - thresholdFrames;
|
|
48
|
+
this.#stalled = !(config.startUnstalled ?? false);
|
|
49
|
+
this.#diagEnabled = config.diag ?? false;
|
|
50
|
+
this.#buffer = Array.from(
|
|
70
51
|
{ length: config.channels },
|
|
71
52
|
() => new Float32Array(capacityFrames)
|
|
72
|
-
)
|
|
53
|
+
);
|
|
73
54
|
}
|
|
74
55
|
get stalled() {
|
|
75
|
-
return
|
|
56
|
+
return this.#stalled;
|
|
76
57
|
}
|
|
77
58
|
get startupFrames() {
|
|
78
|
-
return
|
|
59
|
+
return this.#startupFrames;
|
|
79
60
|
}
|
|
80
61
|
get capacity() {
|
|
81
|
-
return
|
|
62
|
+
return this.#buffer[0].length;
|
|
82
63
|
}
|
|
83
64
|
get length() {
|
|
84
|
-
return
|
|
65
|
+
return this.#writeIndex - this.#readIndex;
|
|
85
66
|
}
|
|
86
67
|
/** Timestamp of the next media sample after the PCM already emitted. */
|
|
87
68
|
get timestampUs() {
|
|
88
|
-
const anchor =
|
|
89
|
-
if (!anchor || anchor.sampleIndex >
|
|
90
|
-
return anchor.timestampUs + (
|
|
69
|
+
const anchor = this.#timestampAnchors[0];
|
|
70
|
+
if (!anchor || anchor.sampleIndex > this.#readIndex) return void 0;
|
|
71
|
+
return anchor.timestampUs + (this.#readIndex - anchor.sampleIndex) / this.rate * 1e6;
|
|
91
72
|
}
|
|
92
73
|
write(timestampUs, data) {
|
|
93
74
|
if (data.length !== this.channels) throw new Error("wrong channel count");
|
|
@@ -97,68 +78,68 @@ var AudioRingBuffer = class {
|
|
|
97
78
|
if (data.some((channel) => channel.length !== samples)) {
|
|
98
79
|
throw new Error("mismatched channel lengths");
|
|
99
80
|
}
|
|
100
|
-
const start =
|
|
81
|
+
const start = this.#writeIndex;
|
|
101
82
|
const end = start + samples;
|
|
102
|
-
|
|
103
|
-
const overflow = end -
|
|
83
|
+
this.#timestampAnchors.push({ sampleIndex: start, timestampUs });
|
|
84
|
+
const overflow = end - this.#readIndex - this.capacity;
|
|
104
85
|
if (overflow > 0) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
86
|
+
this.#stalled = false;
|
|
87
|
+
this.#hasStarted = true;
|
|
88
|
+
this.#readIndex += overflow;
|
|
89
|
+
if (this.#diagEnabled) this.#diagOverflowSamples += overflow;
|
|
90
|
+
this.#pruneTimestampAnchors();
|
|
110
91
|
}
|
|
111
|
-
const refillFrames =
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
const refillFrames = this.#hasStarted ? this.#resumeFrames : this.#startupFrames;
|
|
93
|
+
if (this.#stalled && end - this.#readIndex >= refillFrames) {
|
|
94
|
+
this.#stalled = false;
|
|
95
|
+
this.#hasStarted = true;
|
|
115
96
|
}
|
|
116
97
|
for (let channel = 0; channel < this.channels; channel++) {
|
|
117
98
|
const src = data[channel];
|
|
118
|
-
const dst =
|
|
99
|
+
const dst = this.#buffer[channel];
|
|
119
100
|
for (let i = 0; i < samples; i++) {
|
|
120
101
|
dst[(start + i) % dst.length] = src[i];
|
|
121
102
|
}
|
|
122
103
|
}
|
|
123
|
-
|
|
104
|
+
this.#writeIndex = end;
|
|
124
105
|
}
|
|
125
106
|
read(output) {
|
|
126
107
|
if (output.length !== this.channels) throw new Error("wrong channel count");
|
|
127
|
-
if (
|
|
108
|
+
if (this.#stalled) return 0;
|
|
128
109
|
const available = this.length;
|
|
129
110
|
if (available === 0) {
|
|
130
|
-
|
|
111
|
+
this.#stalled = true;
|
|
131
112
|
return 0;
|
|
132
113
|
}
|
|
133
114
|
const requested = output[0].length;
|
|
134
115
|
const readSamples = Math.min(available, requested);
|
|
135
116
|
for (let channel = 0; channel < this.channels; channel++) {
|
|
136
117
|
const dst = output[channel];
|
|
137
|
-
const src =
|
|
118
|
+
const src = this.#buffer[channel];
|
|
138
119
|
for (let i = 0; i < readSamples; i++) {
|
|
139
|
-
dst[i] = src[(
|
|
120
|
+
dst[i] = src[(this.#readIndex + i) % src.length];
|
|
140
121
|
}
|
|
141
122
|
for (let i = readSamples; i < requested; i++) dst[i] = 0;
|
|
142
123
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (readSamples < requested)
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
124
|
+
this.#readIndex += readSamples;
|
|
125
|
+
this.#pruneTimestampAnchors();
|
|
126
|
+
if (readSamples < requested) this.#stalled = true;
|
|
127
|
+
if (this.#diagEnabled) {
|
|
128
|
+
this.#diagReads++;
|
|
129
|
+
this.#diagFillSum += available;
|
|
149
130
|
}
|
|
150
131
|
return readSamples;
|
|
151
132
|
}
|
|
152
133
|
/** Clear queued PCM; optionally treat the next fill as post-start recovery. */
|
|
153
134
|
reset(startUnstalled = false, resumeAfterReset = false) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
135
|
+
this.#writeIndex = 0;
|
|
136
|
+
this.#readIndex = 0;
|
|
137
|
+
this.#stalled = !startUnstalled;
|
|
138
|
+
this.#hasStarted = startUnstalled || resumeAfterReset;
|
|
139
|
+
this.#timestampAnchors = [];
|
|
140
|
+
this.#diagReads = 0;
|
|
141
|
+
this.#diagFillSum = 0;
|
|
142
|
+
this.#diagOverflowSamples = 0;
|
|
162
143
|
}
|
|
163
144
|
/**
|
|
164
145
|
* Change the startup latency while preserving the configured storage
|
|
@@ -173,18 +154,18 @@ var AudioRingBuffer = class {
|
|
|
173
154
|
if (!Number.isSafeInteger(newStartupFrames) || newStartupFrames <= 0) {
|
|
174
155
|
throw new Error("invalid latency");
|
|
175
156
|
}
|
|
176
|
-
const newResumeFrames =
|
|
177
|
-
const newCapacity = Math.max(newStartupFrames, newResumeFrames) +
|
|
157
|
+
const newResumeFrames = this.#resumeFollowsStartup ? newStartupFrames : this.#resumeFrames;
|
|
158
|
+
const newCapacity = Math.max(newStartupFrames, newResumeFrames) + this.#capacityHeadroomFrames;
|
|
178
159
|
if (!Number.isSafeInteger(newCapacity) || newCapacity < Math.max(newStartupFrames, newResumeFrames)) {
|
|
179
160
|
throw new Error("invalid capacity frames");
|
|
180
161
|
}
|
|
181
|
-
const wasStalled =
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
const wasStalled = this.#stalled;
|
|
163
|
+
this.#startupFrames = newStartupFrames;
|
|
164
|
+
this.#resumeFrames = newResumeFrames;
|
|
184
165
|
if (newCapacity === this.capacity) {
|
|
185
|
-
if (wasStalled && this.length >=
|
|
186
|
-
|
|
187
|
-
|
|
166
|
+
if (wasStalled && this.length >= this.#refillFrames()) {
|
|
167
|
+
this.#stalled = false;
|
|
168
|
+
this.#hasStarted = true;
|
|
188
169
|
}
|
|
189
170
|
return;
|
|
190
171
|
}
|
|
@@ -193,61 +174,46 @@ var AudioRingBuffer = class {
|
|
|
193
174
|
() => new Float32Array(newCapacity)
|
|
194
175
|
);
|
|
195
176
|
const samplesToKeep = Math.min(this.length, newCapacity);
|
|
196
|
-
const copyStart =
|
|
177
|
+
const copyStart = this.#writeIndex - samplesToKeep;
|
|
197
178
|
for (let channel = 0; channel < this.channels; channel++) {
|
|
198
|
-
const src =
|
|
179
|
+
const src = this.#buffer[channel];
|
|
199
180
|
const dst = next[channel];
|
|
200
181
|
for (let i = 0; i < samplesToKeep; i++) {
|
|
201
182
|
const sampleIndex = copyStart + i;
|
|
202
183
|
dst[sampleIndex % dst.length] = src[sampleIndex % src.length];
|
|
203
184
|
}
|
|
204
185
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!
|
|
186
|
+
this.#buffer = next;
|
|
187
|
+
this.#readIndex = copyStart;
|
|
188
|
+
this.#pruneTimestampAnchors();
|
|
189
|
+
this.#stalled = samplesToKeep === 0 || wasStalled && samplesToKeep < this.#refillFrames();
|
|
190
|
+
if (!this.#stalled) this.#hasStarted = true;
|
|
210
191
|
}
|
|
211
192
|
getDiagAndReset() {
|
|
212
|
-
if (!
|
|
193
|
+
if (!this.#diagEnabled || this.#diagReads === 0) return null;
|
|
213
194
|
const result = {
|
|
214
|
-
reads:
|
|
215
|
-
avgFill: Math.round(
|
|
195
|
+
reads: this.#diagReads,
|
|
196
|
+
avgFill: Math.round(this.#diagFillSum / this.#diagReads),
|
|
216
197
|
capacity: this.capacity,
|
|
217
198
|
skips: 0,
|
|
218
199
|
dups: 0,
|
|
219
|
-
overflowSamples:
|
|
200
|
+
overflowSamples: this.#diagOverflowSamples
|
|
220
201
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
202
|
+
this.#diagReads = 0;
|
|
203
|
+
this.#diagFillSum = 0;
|
|
204
|
+
this.#diagOverflowSamples = 0;
|
|
224
205
|
return result;
|
|
225
206
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
_timestampAnchors = new WeakMap();
|
|
237
|
-
_diagEnabled = new WeakMap();
|
|
238
|
-
_diagReads = new WeakMap();
|
|
239
|
-
_diagFillSum = new WeakMap();
|
|
240
|
-
_diagOverflowSamples = new WeakMap();
|
|
241
|
-
_AudioRingBuffer_instances = new WeakSet();
|
|
242
|
-
pruneTimestampAnchors_fn = function() {
|
|
243
|
-
let keep = 0;
|
|
244
|
-
while (keep + 1 < __privateGet(this, _timestampAnchors).length && __privateGet(this, _timestampAnchors)[keep + 1].sampleIndex <= __privateGet(this, _readIndex)) {
|
|
245
|
-
keep++;
|
|
246
|
-
}
|
|
247
|
-
if (keep > 0) __privateGet(this, _timestampAnchors).splice(0, keep);
|
|
248
|
-
};
|
|
249
|
-
refillFrames_fn = function() {
|
|
250
|
-
return __privateGet(this, _hasStarted) ? __privateGet(this, _resumeFrames) : __privateGet(this, _startupFrames);
|
|
207
|
+
#pruneTimestampAnchors() {
|
|
208
|
+
let keep = 0;
|
|
209
|
+
while (keep + 1 < this.#timestampAnchors.length && this.#timestampAnchors[keep + 1].sampleIndex <= this.#readIndex) {
|
|
210
|
+
keep++;
|
|
211
|
+
}
|
|
212
|
+
if (keep > 0) this.#timestampAnchors.splice(0, keep);
|
|
213
|
+
}
|
|
214
|
+
#refillFrames() {
|
|
215
|
+
return this.#hasStarted ? this.#resumeFrames : this.#startupFrames;
|
|
216
|
+
}
|
|
251
217
|
};
|
|
252
218
|
|
|
253
219
|
// src/audio/audio-buffer-plan.ts
|
|
@@ -283,12 +249,11 @@ function deriveAudioBurstBufferPlan(input) {
|
|
|
283
249
|
import { MmtpDecodePipeline } from "@blockcast/mmt-container/mmtp-decode-pipeline";
|
|
284
250
|
|
|
285
251
|
// src/pacing/frame-pacer.ts
|
|
286
|
-
var _frameCount, _anchorWallMs, _anchored;
|
|
287
252
|
var FrameCountPacer = class {
|
|
253
|
+
#frameCount = 0;
|
|
254
|
+
#anchorWallMs = 0;
|
|
255
|
+
#anchored = false;
|
|
288
256
|
constructor(opts) {
|
|
289
|
-
__privateAdd(this, _frameCount, 0);
|
|
290
|
-
__privateAdd(this, _anchorWallMs, 0);
|
|
291
|
-
__privateAdd(this, _anchored, false);
|
|
292
257
|
if (!(opts.frameDurationMs > 0)) {
|
|
293
258
|
throw new Error("frameDurationMs must be > 0");
|
|
294
259
|
}
|
|
@@ -298,11 +263,11 @@ var FrameCountPacer = class {
|
|
|
298
263
|
}
|
|
299
264
|
/** Call on every emitted decoded frame (AU or picture). Anchors on first call. */
|
|
300
265
|
notifyFrame() {
|
|
301
|
-
if (!
|
|
302
|
-
|
|
303
|
-
|
|
266
|
+
if (!this.#anchored) {
|
|
267
|
+
this.#anchorWallMs = now();
|
|
268
|
+
this.#anchored = true;
|
|
304
269
|
}
|
|
305
|
-
|
|
270
|
+
this.#frameCount++;
|
|
306
271
|
}
|
|
307
272
|
/**
|
|
308
273
|
* Compute sleep ms before reading the next MoQ object.
|
|
@@ -336,39 +301,36 @@ var FrameCountPacer = class {
|
|
|
336
301
|
*/
|
|
337
302
|
computeSleepMs(observedDelayMs) {
|
|
338
303
|
if (this.thresholdMs <= 0) return 0;
|
|
339
|
-
if (!
|
|
340
|
-
if (
|
|
304
|
+
if (!this.#anchored) return 0;
|
|
305
|
+
if (this.#frameCount <= this.warmupFrames) return 0;
|
|
341
306
|
if (observedDelayMs !== void 0 && Number.isFinite(observedDelayMs) && observedDelayMs > this.thresholdMs) {
|
|
342
307
|
return 0;
|
|
343
308
|
}
|
|
344
|
-
const mediaMs =
|
|
345
|
-
const wallMs = now() -
|
|
309
|
+
const mediaMs = this.#frameCount * this.frameDurationMs;
|
|
310
|
+
const wallMs = now() - this.#anchorWallMs;
|
|
346
311
|
const aheadMs = mediaMs - wallMs;
|
|
347
312
|
if (aheadMs <= this.thresholdMs) return 0;
|
|
348
313
|
return aheadMs - this.thresholdMs / 2;
|
|
349
314
|
}
|
|
350
315
|
/** Reset anchor + counter. Call on source switch (MoQ ↔ multicast). */
|
|
351
316
|
reset() {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
317
|
+
this.#frameCount = 0;
|
|
318
|
+
this.#anchorWallMs = 0;
|
|
319
|
+
this.#anchored = false;
|
|
355
320
|
}
|
|
356
321
|
get snapshot() {
|
|
357
|
-
const mediaMs =
|
|
358
|
-
const wallMs =
|
|
322
|
+
const mediaMs = this.#frameCount * this.frameDurationMs;
|
|
323
|
+
const wallMs = this.#anchored ? now() - this.#anchorWallMs : 0;
|
|
359
324
|
return {
|
|
360
|
-
frameCount:
|
|
325
|
+
frameCount: this.#frameCount,
|
|
361
326
|
mediaMs,
|
|
362
327
|
wallMs,
|
|
363
328
|
aheadMs: mediaMs - wallMs,
|
|
364
329
|
thresholdMs: this.thresholdMs,
|
|
365
|
-
anchored:
|
|
330
|
+
anchored: this.#anchored
|
|
366
331
|
};
|
|
367
332
|
}
|
|
368
333
|
};
|
|
369
|
-
_frameCount = new WeakMap();
|
|
370
|
-
_anchorWallMs = new WeakMap();
|
|
371
|
-
_anchored = new WeakMap();
|
|
372
334
|
function now() {
|
|
373
335
|
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
374
336
|
return performance.now();
|
|
@@ -399,260 +361,226 @@ function workletModuleUrl() {
|
|
|
399
361
|
var WORKLET_PROCESSOR_NAME = "mmt-render";
|
|
400
362
|
|
|
401
363
|
// src/audio/audio-render-pipeline.ts
|
|
402
|
-
var _context, _pacer, _mapper, _codec, _packetId, _sampleRate, _channels, _description, _latencyMs, _startupFrames2, _startupThresholdFrames, _capacityFrames, _diag, _warmupFrames, _onState, _onDiag, _onError, _reassemblyPolicy, _decoder, _pipeline, _worklet, _fecClient, _closed, _readyPromise, _workletGeneration, _startupReleased, _AudioRenderPipeline_instances, initialize_fn, onDecoded_fn;
|
|
403
364
|
var AudioRenderPipeline = class {
|
|
365
|
+
#context;
|
|
366
|
+
#pacer;
|
|
367
|
+
#mapper;
|
|
368
|
+
#codec;
|
|
369
|
+
#packetId;
|
|
370
|
+
#sampleRate;
|
|
371
|
+
#channels;
|
|
372
|
+
#description;
|
|
373
|
+
#latencyMs;
|
|
374
|
+
#startupFrames;
|
|
375
|
+
#startupThresholdFrames;
|
|
376
|
+
#capacityFrames;
|
|
377
|
+
#diag;
|
|
378
|
+
#warmupFrames;
|
|
379
|
+
#onState;
|
|
380
|
+
#onDiag;
|
|
381
|
+
#onError;
|
|
382
|
+
#reassemblyPolicy;
|
|
383
|
+
#decoder = null;
|
|
384
|
+
#pipeline = null;
|
|
385
|
+
#worklet = null;
|
|
386
|
+
#fecClient;
|
|
387
|
+
#closed = false;
|
|
388
|
+
#readyPromise = null;
|
|
389
|
+
#workletGeneration = 0;
|
|
390
|
+
#startupReleased = false;
|
|
404
391
|
constructor(opts) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
__privateAdd(this, _reassemblyPolicy);
|
|
424
|
-
__privateAdd(this, _decoder, null);
|
|
425
|
-
__privateAdd(this, _pipeline, null);
|
|
426
|
-
__privateAdd(this, _worklet, null);
|
|
427
|
-
__privateAdd(this, _fecClient);
|
|
428
|
-
__privateAdd(this, _closed, false);
|
|
429
|
-
__privateAdd(this, _readyPromise, null);
|
|
430
|
-
__privateAdd(this, _workletGeneration, 0);
|
|
431
|
-
__privateAdd(this, _startupReleased, false);
|
|
432
|
-
__privateSet(this, _context, opts.context);
|
|
433
|
-
__privateSet(this, _mapper, opts.mapper);
|
|
434
|
-
__privateSet(this, _codec, opts.codec);
|
|
435
|
-
__privateSet(this, _packetId, opts.packetId);
|
|
436
|
-
__privateSet(this, _sampleRate, opts.sampleRate);
|
|
437
|
-
__privateSet(this, _channels, opts.channels);
|
|
438
|
-
__privateSet(this, _description, opts.description);
|
|
439
|
-
__privateSet(this, _latencyMs, opts.latencyMs);
|
|
440
|
-
__privateSet(this, _startupFrames2, opts.startupFrames);
|
|
441
|
-
__privateSet(this, _startupThresholdFrames, opts.startupFrames ?? Math.ceil(opts.sampleRate * (opts.latencyMs / 1e3)));
|
|
442
|
-
__privateSet(this, _capacityFrames, opts.capacityFrames);
|
|
443
|
-
__privateSet(this, _diag, opts.diag ?? false);
|
|
444
|
-
__privateSet(this, _warmupFrames, opts.warmupFrames ?? 0);
|
|
445
|
-
__privateSet(this, _onState, opts.onState);
|
|
446
|
-
__privateSet(this, _onDiag, opts.onDiag);
|
|
447
|
-
__privateSet(this, _onError, opts.onError);
|
|
448
|
-
__privateSet(this, _fecClient, opts.fecClient);
|
|
449
|
-
__privateSet(this, _reassemblyPolicy, opts.reassemblyPolicy);
|
|
392
|
+
this.#context = opts.context;
|
|
393
|
+
this.#mapper = opts.mapper;
|
|
394
|
+
this.#codec = opts.codec;
|
|
395
|
+
this.#packetId = opts.packetId;
|
|
396
|
+
this.#sampleRate = opts.sampleRate;
|
|
397
|
+
this.#channels = opts.channels;
|
|
398
|
+
this.#description = opts.description;
|
|
399
|
+
this.#latencyMs = opts.latencyMs;
|
|
400
|
+
this.#startupFrames = opts.startupFrames;
|
|
401
|
+
this.#startupThresholdFrames = opts.startupFrames ?? Math.ceil(opts.sampleRate * (opts.latencyMs / 1e3));
|
|
402
|
+
this.#capacityFrames = opts.capacityFrames;
|
|
403
|
+
this.#diag = opts.diag ?? false;
|
|
404
|
+
this.#warmupFrames = opts.warmupFrames ?? 0;
|
|
405
|
+
this.#onState = opts.onState;
|
|
406
|
+
this.#onDiag = opts.onDiag;
|
|
407
|
+
this.#onError = opts.onError;
|
|
408
|
+
this.#fecClient = opts.fecClient;
|
|
409
|
+
this.#reassemblyPolicy = opts.reassemblyPolicy;
|
|
450
410
|
const auDurationMs = auDurationMsFromCodec(opts.codec, opts.sampleRate);
|
|
451
|
-
|
|
411
|
+
this.#pacer = new FrameCountPacer({
|
|
452
412
|
frameDurationMs: auDurationMs,
|
|
453
413
|
mapper: opts.mapper,
|
|
454
|
-
warmupFrames:
|
|
455
|
-
})
|
|
414
|
+
warmupFrames: this.#warmupFrames
|
|
415
|
+
});
|
|
456
416
|
}
|
|
457
417
|
/** Load worklet module + create decoder + pipeline. Call once. */
|
|
458
418
|
async ready() {
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
return
|
|
419
|
+
if (this.#readyPromise) return this.#readyPromise;
|
|
420
|
+
this.#readyPromise = this.#initialize();
|
|
421
|
+
return this.#readyPromise;
|
|
422
|
+
}
|
|
423
|
+
async #initialize() {
|
|
424
|
+
await this.#context.audioWorklet.addModule(workletModuleUrl());
|
|
425
|
+
const worklet = new AudioWorkletNode(this.#context, WORKLET_PROCESSOR_NAME, {
|
|
426
|
+
numberOfInputs: 0,
|
|
427
|
+
numberOfOutputs: 1,
|
|
428
|
+
outputChannelCount: [this.#channels]
|
|
429
|
+
});
|
|
430
|
+
const initMsg = {
|
|
431
|
+
type: "init",
|
|
432
|
+
generation: ++this.#workletGeneration,
|
|
433
|
+
channels: this.#channels,
|
|
434
|
+
rate: this.#sampleRate,
|
|
435
|
+
latencyMs: this.#latencyMs,
|
|
436
|
+
startupFrames: this.#startupFrames,
|
|
437
|
+
capacityFrames: this.#capacityFrames,
|
|
438
|
+
holdUntilStart: true,
|
|
439
|
+
diag: this.#diag
|
|
440
|
+
};
|
|
441
|
+
worklet.port.postMessage(initMsg);
|
|
442
|
+
worklet.port.onmessage = (ev) => {
|
|
443
|
+
const m = ev.data;
|
|
444
|
+
if (m.type === "state") {
|
|
445
|
+
if (m.generation !== this.#workletGeneration) return;
|
|
446
|
+
this.#onState?.({
|
|
447
|
+
timestampUs: m.timestamp,
|
|
448
|
+
stalled: m.stalled,
|
|
449
|
+
bufferedFrames: m.bufferedFrames,
|
|
450
|
+
writtenFrames: m.writtenFrames
|
|
451
|
+
});
|
|
452
|
+
} else if (m.type === "ringBufDiag") {
|
|
453
|
+
if (m.generation !== this.#workletGeneration) return;
|
|
454
|
+
this.#onDiag?.({
|
|
455
|
+
reads: m.reads,
|
|
456
|
+
avgFill: m.avgFill,
|
|
457
|
+
capacity: m.capacity,
|
|
458
|
+
skipsPerSec: m.skipsPerSec,
|
|
459
|
+
dupsPerSec: m.dupsPerSec,
|
|
460
|
+
overflowSamplesPerSec: m.overflowSamplesPerSec,
|
|
461
|
+
underflowSamplesPerSec: m.underflowSamplesPerSec
|
|
462
|
+
});
|
|
463
|
+
} else if (m.type === "writeAck") {
|
|
464
|
+
if (m.generation !== this.#workletGeneration || this.#startupReleased || m.bufferedFrames < this.#startupThresholdFrames) return;
|
|
465
|
+
this.#startupReleased = true;
|
|
466
|
+
const start = {
|
|
467
|
+
type: "start",
|
|
468
|
+
generation: this.#workletGeneration
|
|
469
|
+
};
|
|
470
|
+
worklet.port.postMessage(start);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
this.#worklet = worklet;
|
|
474
|
+
const decoder = new AudioDecoder({
|
|
475
|
+
output: (data) => this.#onDecoded(data),
|
|
476
|
+
error: (err) => this.#onError?.(err)
|
|
477
|
+
});
|
|
478
|
+
const config = {
|
|
479
|
+
codec: this.#codec,
|
|
480
|
+
sampleRate: this.#sampleRate,
|
|
481
|
+
numberOfChannels: this.#channels,
|
|
482
|
+
...this.#description ? { description: this.#description } : {}
|
|
483
|
+
};
|
|
484
|
+
decoder.configure(config);
|
|
485
|
+
this.#decoder = decoder;
|
|
486
|
+
const pipeline = new MmtpDecodePipeline();
|
|
487
|
+
pipeline.configure({
|
|
488
|
+
codec: this.#codec,
|
|
489
|
+
packetId: this.#packetId,
|
|
490
|
+
description: this.#description,
|
|
491
|
+
reorderWindow: 0,
|
|
492
|
+
// audio doesn't need reorder — AAC/Opus frames are self-contained
|
|
493
|
+
reassemblyPolicy: this.#reassemblyPolicy
|
|
494
|
+
});
|
|
495
|
+
if (this.#fecClient) pipeline.setFecClient(this.#fecClient);
|
|
496
|
+
pipeline.onFrame((codecData, meta) => {
|
|
497
|
+
if (!this.#decoder || this.#decoder.state !== "configured") return;
|
|
498
|
+
if (codecData.byteLength === 0) return;
|
|
499
|
+
this.#pacer.notifyFrame();
|
|
500
|
+
try {
|
|
501
|
+
this.#decoder.decode(new EncodedAudioChunk({
|
|
502
|
+
type: "key",
|
|
503
|
+
data: codecData,
|
|
504
|
+
timestamp: meta.timestamp
|
|
505
|
+
}));
|
|
506
|
+
} catch (err) {
|
|
507
|
+
this.#onError?.(err);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
this.#pipeline = pipeline;
|
|
462
511
|
}
|
|
463
512
|
/** Feed one MoQ object (raw MMTP packet). */
|
|
464
513
|
feedMoqObject(packet) {
|
|
465
|
-
|
|
514
|
+
this.#pipeline?.feedMoqObject(packet);
|
|
466
515
|
}
|
|
467
516
|
/** ms to sleep before the next object read. 0 when not anchored / not ahead. */
|
|
468
517
|
paceSleepMs() {
|
|
469
|
-
return
|
|
518
|
+
return this.#pacer.computeSleepMs();
|
|
470
519
|
}
|
|
471
520
|
/** Pacer snapshot (for diagnostics / UI). */
|
|
472
521
|
get pacerSnapshot() {
|
|
473
|
-
return
|
|
522
|
+
return this.#pacer.snapshot;
|
|
474
523
|
}
|
|
475
524
|
/** Reset pacer on source switch (MoQ ↔ multicast). */
|
|
476
525
|
resetPacer() {
|
|
477
|
-
|
|
526
|
+
this.#pacer.reset();
|
|
478
527
|
}
|
|
479
528
|
/** Drop queued PCM and its timestamp epoch at an explicit source boundary. */
|
|
480
529
|
resetPlayout(startUnstalled = false) {
|
|
481
|
-
|
|
482
|
-
|
|
530
|
+
this.#pacer.reset();
|
|
531
|
+
this.#startupReleased = true;
|
|
483
532
|
const message = {
|
|
484
533
|
type: "reset",
|
|
485
|
-
generation: ++
|
|
534
|
+
generation: ++this.#workletGeneration,
|
|
486
535
|
startUnstalled
|
|
487
536
|
};
|
|
488
|
-
|
|
537
|
+
this.#worklet?.port.postMessage(message);
|
|
489
538
|
}
|
|
490
539
|
/** Node to connect to destination / gain. */
|
|
491
540
|
get rootNode() {
|
|
492
|
-
if (!
|
|
493
|
-
return
|
|
541
|
+
if (!this.#worklet) throw new Error("AudioRenderPipeline.ready() must resolve first");
|
|
542
|
+
return this.#worklet;
|
|
494
543
|
}
|
|
495
544
|
close() {
|
|
496
|
-
if (
|
|
497
|
-
|
|
545
|
+
if (this.#closed) return;
|
|
546
|
+
this.#closed = true;
|
|
498
547
|
try {
|
|
499
|
-
|
|
548
|
+
this.#decoder?.close();
|
|
500
549
|
} catch {
|
|
501
550
|
}
|
|
502
551
|
try {
|
|
503
|
-
|
|
552
|
+
this.#pipeline?.dispose();
|
|
504
553
|
} catch {
|
|
505
554
|
}
|
|
506
555
|
try {
|
|
507
|
-
|
|
556
|
+
this.#worklet?.disconnect();
|
|
508
557
|
} catch {
|
|
509
558
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
_packetId = new WeakMap();
|
|
520
|
-
_sampleRate = new WeakMap();
|
|
521
|
-
_channels = new WeakMap();
|
|
522
|
-
_description = new WeakMap();
|
|
523
|
-
_latencyMs = new WeakMap();
|
|
524
|
-
_startupFrames2 = new WeakMap();
|
|
525
|
-
_startupThresholdFrames = new WeakMap();
|
|
526
|
-
_capacityFrames = new WeakMap();
|
|
527
|
-
_diag = new WeakMap();
|
|
528
|
-
_warmupFrames = new WeakMap();
|
|
529
|
-
_onState = new WeakMap();
|
|
530
|
-
_onDiag = new WeakMap();
|
|
531
|
-
_onError = new WeakMap();
|
|
532
|
-
_reassemblyPolicy = new WeakMap();
|
|
533
|
-
_decoder = new WeakMap();
|
|
534
|
-
_pipeline = new WeakMap();
|
|
535
|
-
_worklet = new WeakMap();
|
|
536
|
-
_fecClient = new WeakMap();
|
|
537
|
-
_closed = new WeakMap();
|
|
538
|
-
_readyPromise = new WeakMap();
|
|
539
|
-
_workletGeneration = new WeakMap();
|
|
540
|
-
_startupReleased = new WeakMap();
|
|
541
|
-
_AudioRenderPipeline_instances = new WeakSet();
|
|
542
|
-
initialize_fn = async function() {
|
|
543
|
-
await __privateGet(this, _context).audioWorklet.addModule(workletModuleUrl());
|
|
544
|
-
const worklet = new AudioWorkletNode(__privateGet(this, _context), WORKLET_PROCESSOR_NAME, {
|
|
545
|
-
numberOfInputs: 0,
|
|
546
|
-
numberOfOutputs: 1,
|
|
547
|
-
outputChannelCount: [__privateGet(this, _channels)]
|
|
548
|
-
});
|
|
549
|
-
const initMsg = {
|
|
550
|
-
type: "init",
|
|
551
|
-
generation: ++__privateWrapper(this, _workletGeneration)._,
|
|
552
|
-
channels: __privateGet(this, _channels),
|
|
553
|
-
rate: __privateGet(this, _sampleRate),
|
|
554
|
-
latencyMs: __privateGet(this, _latencyMs),
|
|
555
|
-
startupFrames: __privateGet(this, _startupFrames2),
|
|
556
|
-
capacityFrames: __privateGet(this, _capacityFrames),
|
|
557
|
-
holdUntilStart: true,
|
|
558
|
-
diag: __privateGet(this, _diag)
|
|
559
|
-
};
|
|
560
|
-
worklet.port.postMessage(initMsg);
|
|
561
|
-
worklet.port.onmessage = (ev) => {
|
|
562
|
-
var _a, _b;
|
|
563
|
-
const m = ev.data;
|
|
564
|
-
if (m.type === "state") {
|
|
565
|
-
if (m.generation !== __privateGet(this, _workletGeneration)) return;
|
|
566
|
-
(_a = __privateGet(this, _onState)) == null ? void 0 : _a.call(this, {
|
|
567
|
-
timestampUs: m.timestamp,
|
|
568
|
-
stalled: m.stalled,
|
|
569
|
-
bufferedFrames: m.bufferedFrames,
|
|
570
|
-
writtenFrames: m.writtenFrames
|
|
571
|
-
});
|
|
572
|
-
} else if (m.type === "ringBufDiag") {
|
|
573
|
-
if (m.generation !== __privateGet(this, _workletGeneration)) return;
|
|
574
|
-
(_b = __privateGet(this, _onDiag)) == null ? void 0 : _b.call(this, {
|
|
575
|
-
reads: m.reads,
|
|
576
|
-
avgFill: m.avgFill,
|
|
577
|
-
capacity: m.capacity,
|
|
578
|
-
skipsPerSec: m.skipsPerSec,
|
|
579
|
-
dupsPerSec: m.dupsPerSec,
|
|
580
|
-
overflowSamplesPerSec: m.overflowSamplesPerSec,
|
|
581
|
-
underflowSamplesPerSec: m.underflowSamplesPerSec
|
|
582
|
-
});
|
|
583
|
-
} else if (m.type === "writeAck") {
|
|
584
|
-
if (m.generation !== __privateGet(this, _workletGeneration) || __privateGet(this, _startupReleased) || m.bufferedFrames < __privateGet(this, _startupThresholdFrames)) return;
|
|
585
|
-
__privateSet(this, _startupReleased, true);
|
|
586
|
-
const start = {
|
|
587
|
-
type: "start",
|
|
588
|
-
generation: __privateGet(this, _workletGeneration)
|
|
589
|
-
};
|
|
590
|
-
worklet.port.postMessage(start);
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
__privateSet(this, _worklet, worklet);
|
|
594
|
-
const decoder = new AudioDecoder({
|
|
595
|
-
output: (data) => __privateMethod(this, _AudioRenderPipeline_instances, onDecoded_fn).call(this, data),
|
|
596
|
-
error: (err) => {
|
|
597
|
-
var _a;
|
|
598
|
-
return (_a = __privateGet(this, _onError)) == null ? void 0 : _a.call(this, err);
|
|
559
|
+
this.#decoder = null;
|
|
560
|
+
this.#pipeline = null;
|
|
561
|
+
this.#worklet = null;
|
|
562
|
+
}
|
|
563
|
+
// ─── Private ─────────────────────────────────────────────────────
|
|
564
|
+
#onDecoded(sample) {
|
|
565
|
+
if (this.#closed || !this.#worklet) {
|
|
566
|
+
sample.close();
|
|
567
|
+
return;
|
|
599
568
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
};
|
|
607
|
-
decoder.configure(config);
|
|
608
|
-
__privateSet(this, _decoder, decoder);
|
|
609
|
-
const pipeline = new MmtpDecodePipeline();
|
|
610
|
-
pipeline.configure({
|
|
611
|
-
codec: __privateGet(this, _codec),
|
|
612
|
-
packetId: __privateGet(this, _packetId),
|
|
613
|
-
description: __privateGet(this, _description),
|
|
614
|
-
reorderWindow: 0,
|
|
615
|
-
// audio doesn't need reorder — AAC/Opus frames are self-contained
|
|
616
|
-
reassemblyPolicy: __privateGet(this, _reassemblyPolicy)
|
|
617
|
-
});
|
|
618
|
-
if (__privateGet(this, _fecClient)) pipeline.setFecClient(__privateGet(this, _fecClient));
|
|
619
|
-
pipeline.onFrame((codecData, meta) => {
|
|
620
|
-
var _a;
|
|
621
|
-
if (!__privateGet(this, _decoder) || __privateGet(this, _decoder).state !== "configured") return;
|
|
622
|
-
if (codecData.byteLength === 0) return;
|
|
623
|
-
__privateGet(this, _pacer).notifyFrame();
|
|
624
|
-
try {
|
|
625
|
-
__privateGet(this, _decoder).decode(new EncodedAudioChunk({
|
|
626
|
-
type: "key",
|
|
627
|
-
data: codecData,
|
|
628
|
-
timestamp: meta.timestamp
|
|
629
|
-
}));
|
|
630
|
-
} catch (err) {
|
|
631
|
-
(_a = __privateGet(this, _onError)) == null ? void 0 : _a.call(this, err);
|
|
569
|
+
const timestamp = sample.timestamp;
|
|
570
|
+
const channelData = [];
|
|
571
|
+
for (let ch = 0; ch < sample.numberOfChannels; ch++) {
|
|
572
|
+
const data = new Float32Array(sample.numberOfFrames);
|
|
573
|
+
sample.copyTo(data, { format: "f32-planar", planeIndex: ch });
|
|
574
|
+
channelData.push(data);
|
|
632
575
|
}
|
|
633
|
-
});
|
|
634
|
-
__privateSet(this, _pipeline, pipeline);
|
|
635
|
-
};
|
|
636
|
-
// ─── Private ─────────────────────────────────────────────────────
|
|
637
|
-
onDecoded_fn = function(sample) {
|
|
638
|
-
if (__privateGet(this, _closed) || !__privateGet(this, _worklet)) {
|
|
639
576
|
sample.close();
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
channelData.push(data);
|
|
648
|
-
}
|
|
649
|
-
sample.close();
|
|
650
|
-
const msg = {
|
|
651
|
-
type: "data",
|
|
652
|
-
data: channelData,
|
|
653
|
-
timestamp
|
|
654
|
-
};
|
|
655
|
-
__privateGet(this, _worklet).port.postMessage(msg, channelData.map((d) => d.buffer));
|
|
577
|
+
const msg = {
|
|
578
|
+
type: "data",
|
|
579
|
+
data: channelData,
|
|
580
|
+
timestamp
|
|
581
|
+
};
|
|
582
|
+
this.#worklet.port.postMessage(msg, channelData.map((d) => d.buffer));
|
|
583
|
+
}
|
|
656
584
|
};
|
|
657
585
|
export {
|
|
658
586
|
AudioRenderPipeline,
|