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