@rivmux/runtime-worker 0.4.0 → 1.0.0-rc.1
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 +7 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -8
- package/dist/index.js.map +1 -1
- package/dist/rivmux-runtime-worker.d.ts +57 -40
- package/dist/rivmux-runtime-worker.d.ts.map +1 -1
- package/dist/rivmux-runtime-worker.js +1026 -414
- package/dist/rivmux-runtime-worker.js.map +1 -1
- package/dist/rivmux-transmux-core.wasm +0 -0
- package/package.json +5 -9
|
@@ -141,46 +141,44 @@ const PLAYBACK_RATE_RESTORE_THRESHOLD_SECONDS = .1;
|
|
|
141
141
|
const SEEK_COOLDOWN_MS = 1e3;
|
|
142
142
|
const SEEK_MIN_DELTA_SECONDS = .1;
|
|
143
143
|
//#endregion
|
|
144
|
-
//#region src/loader/retry-policy.ts
|
|
145
|
-
function createRetryPolicy(input) {
|
|
146
|
-
return {
|
|
147
|
-
maxAttempts: clampInteger(input?.maxAttempts, 1),
|
|
148
|
-
backoffMs: clampInteger(input?.backoffMs, 0)
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function getRetryDelayMs(policy, attempt) {
|
|
152
|
-
if (policy.backoffMs === 0) return 0;
|
|
153
|
-
return policy.backoffMs * Math.max(1, attempt);
|
|
154
|
-
}
|
|
155
|
-
function clampInteger(value, minimum) {
|
|
156
|
-
if (value === void 0 || !Number.isFinite(value)) return minimum;
|
|
157
|
-
return Math.max(minimum, Math.trunc(value));
|
|
158
|
-
}
|
|
159
|
-
//#endregion
|
|
160
144
|
//#region src/loader/http-flv-loader.ts
|
|
145
|
+
/** Structured failure raised by one HTTP-FLV connection. */
|
|
161
146
|
var HttpFlvLoaderError = class extends Error {
|
|
162
147
|
code;
|
|
148
|
+
phase;
|
|
149
|
+
reason;
|
|
163
150
|
status;
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
cause;
|
|
152
|
+
constructor(code, message, options) {
|
|
153
|
+
const normalizedOptions = typeof options === "number" ? {
|
|
154
|
+
phase: "open",
|
|
155
|
+
reason: "http-status",
|
|
156
|
+
status: options
|
|
157
|
+
} : options ?? { phase: "open" };
|
|
158
|
+
super(message, normalizedOptions.cause === void 0 ? void 0 : { cause: normalizedOptions.cause });
|
|
166
159
|
this.name = "HttpFlvLoaderError";
|
|
167
160
|
this.code = code;
|
|
168
|
-
this.
|
|
161
|
+
this.phase = normalizedOptions.phase;
|
|
162
|
+
this.reason = normalizedOptions.reason;
|
|
163
|
+
this.status = normalizedOptions.status;
|
|
164
|
+
this.cause = normalizedOptions.cause;
|
|
169
165
|
}
|
|
170
166
|
};
|
|
171
167
|
var HttpFlvLoader = class {
|
|
172
168
|
url;
|
|
173
169
|
headers;
|
|
174
170
|
credentials;
|
|
175
|
-
|
|
171
|
+
readIdleTimeoutMs;
|
|
176
172
|
fetchImpl;
|
|
177
173
|
now;
|
|
178
|
-
|
|
174
|
+
setTimer;
|
|
175
|
+
clearTimer;
|
|
179
176
|
abortController;
|
|
180
177
|
reader;
|
|
181
178
|
state = "idle";
|
|
182
179
|
pausedState = false;
|
|
183
180
|
resumeWaiters = [];
|
|
181
|
+
activeReadTimeout;
|
|
184
182
|
mutableStats = {
|
|
185
183
|
bytesReceived: 0,
|
|
186
184
|
currentNetworkSpeed: 0
|
|
@@ -189,10 +187,11 @@ var HttpFlvLoader = class {
|
|
|
189
187
|
this.url = config.url;
|
|
190
188
|
this.headers = config.network.headers;
|
|
191
189
|
this.credentials = config.network.credentials;
|
|
192
|
-
this.
|
|
190
|
+
this.readIdleTimeoutMs = config.network.readIdleTimeoutMs;
|
|
193
191
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
194
192
|
this.now = config.now ?? (() => performance.now());
|
|
195
|
-
this.
|
|
193
|
+
this.setTimer = config.setTimeout ?? ((callback, ms) => setTimeout(callback, ms));
|
|
194
|
+
this.clearTimer = config.clearTimeout ?? ((timer) => clearTimeout(timer));
|
|
196
195
|
}
|
|
197
196
|
get closed() {
|
|
198
197
|
return this.state === "closed";
|
|
@@ -204,33 +203,101 @@ var HttpFlvLoader = class {
|
|
|
204
203
|
return { ...this.mutableStats };
|
|
205
204
|
}
|
|
206
205
|
async open() {
|
|
207
|
-
if (this.state !== "idle") throw new HttpFlvLoaderError("RIVMUX_LOADER_INVALID_STATE", "HTTP Fetch loader can only be opened once.");
|
|
206
|
+
if (this.state !== "idle") throw new HttpFlvLoaderError("RIVMUX_LOADER_INVALID_STATE", "HTTP Fetch loader can only be opened once.", { phase: "open" });
|
|
208
207
|
this.state = "opening";
|
|
209
208
|
this.mutableStats.startedAtMs = this.now();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
const abortController = new AbortController();
|
|
210
|
+
this.abortController = abortController;
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await raceWithAbort(Promise.resolve(this.fetchImpl(this.url, {
|
|
214
|
+
method: "GET",
|
|
215
|
+
headers: createHeaders(this.headers),
|
|
216
|
+
credentials: this.credentials,
|
|
217
|
+
signal: abortController.signal
|
|
218
|
+
})), abortController.signal, (lateResponse) => void cancelResponseBody(lateResponse));
|
|
219
|
+
} catch (cause) {
|
|
220
|
+
if (this.closed || isAbortLikeError(cause)) throw cause;
|
|
221
|
+
throw new HttpFlvLoaderError("RIVMUX_HTTP_NETWORK_ERROR", "HTTP Fetch loader failed to open the stream.", {
|
|
222
|
+
phase: "open",
|
|
223
|
+
reason: "network-error",
|
|
224
|
+
cause
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (this.closed) {
|
|
228
|
+
await cancelResponseBody(response);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
await cancelResponseBody(response);
|
|
233
|
+
throw new HttpFlvLoaderError("RIVMUX_HTTP_STATUS", `HTTP Fetch loader received status ${response.status} ${response.statusText}.`, {
|
|
234
|
+
phase: "open",
|
|
235
|
+
reason: "http-status",
|
|
236
|
+
status: response.status
|
|
237
|
+
});
|
|
219
238
|
}
|
|
239
|
+
if (response.body === null) throw new HttpFlvLoaderError("RIVMUX_HTTP_BODY_UNAVAILABLE", "HTTP Fetch loader response body is unavailable.", { phase: "open" });
|
|
240
|
+
const contentLength = response.headers.get("Content-Length");
|
|
241
|
+
if (contentLength !== null) {
|
|
242
|
+
const parsedContentLength = Number.parseInt(contentLength, 10);
|
|
243
|
+
if (Number.isFinite(parsedContentLength) && parsedContentLength >= 0) this.mutableStats.contentLength = parsedContentLength;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
this.reader = response.body.getReader();
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
await cancelResponseBody(response);
|
|
249
|
+
throw new HttpFlvLoaderError("RIVMUX_HTTP_NETWORK_ERROR", "HTTP Fetch loader could not acquire a stream reader.", {
|
|
250
|
+
phase: "open",
|
|
251
|
+
reason: "network-error",
|
|
252
|
+
cause
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
this.state = "open";
|
|
220
256
|
}
|
|
221
257
|
async read() {
|
|
222
258
|
await this.waitUntilResumed();
|
|
223
259
|
if (this.closed) return null;
|
|
224
260
|
const reader = this.reader;
|
|
225
|
-
|
|
261
|
+
const abortController = this.abortController;
|
|
262
|
+
if (reader === void 0 || abortController === void 0) {
|
|
226
263
|
if (this.closed) return null;
|
|
227
|
-
throw new HttpFlvLoaderError("RIVMUX_LOADER_NOT_OPEN", "HTTP Fetch loader must be opened before read().");
|
|
264
|
+
throw new HttpFlvLoaderError("RIVMUX_LOADER_NOT_OPEN", "HTTP Fetch loader must be opened before read().", { phase: "read" });
|
|
265
|
+
}
|
|
266
|
+
const timeout = new PausableTimeout({
|
|
267
|
+
durationMs: this.readIdleTimeoutMs,
|
|
268
|
+
now: this.now,
|
|
269
|
+
setTimeout: this.setTimer,
|
|
270
|
+
clearTimeout: this.clearTimer
|
|
271
|
+
});
|
|
272
|
+
this.activeReadTimeout = timeout;
|
|
273
|
+
if (this.pausedState) timeout.pause();
|
|
274
|
+
let result;
|
|
275
|
+
try {
|
|
276
|
+
result = await raceWithAbort(Promise.race([reader.read(), timeout.promise]), abortController.signal);
|
|
277
|
+
} catch (cause) {
|
|
278
|
+
if (this.closed || isAbortLikeError(cause)) return null;
|
|
279
|
+
if (cause instanceof ReadIdleTimeoutError) throw new HttpFlvLoaderError("RIVMUX_HTTP_READ_TIMEOUT", `HTTP Fetch loader received no data for ${this.readIdleTimeoutMs} ms.`, {
|
|
280
|
+
phase: "read",
|
|
281
|
+
reason: "read-timeout",
|
|
282
|
+
cause
|
|
283
|
+
});
|
|
284
|
+
throw new HttpFlvLoaderError("RIVMUX_HTTP_READ_FAILED", "HTTP Fetch loader failed while reading the stream.", {
|
|
285
|
+
phase: "read",
|
|
286
|
+
reason: "read-error",
|
|
287
|
+
cause
|
|
288
|
+
});
|
|
289
|
+
} finally {
|
|
290
|
+
timeout.cancel();
|
|
291
|
+
if (this.activeReadTimeout === timeout) this.activeReadTimeout = void 0;
|
|
228
292
|
}
|
|
229
|
-
const result = await reader.read();
|
|
230
293
|
if (result.done) {
|
|
231
294
|
releaseReader(reader);
|
|
232
295
|
if (this.reader === reader) this.reader = void 0;
|
|
233
|
-
return null;
|
|
296
|
+
if (this.closed) return null;
|
|
297
|
+
throw new HttpFlvLoaderError("RIVMUX_HTTP_UNEXPECTED_EOF", "HTTP Fetch loader reached an unexpected end of the live stream.", {
|
|
298
|
+
phase: "read",
|
|
299
|
+
reason: "unexpected-eof"
|
|
300
|
+
});
|
|
234
301
|
}
|
|
235
302
|
const bytes = result.value;
|
|
236
303
|
const receivedAtMs = this.now();
|
|
@@ -245,12 +312,14 @@ var HttpFlvLoader = class {
|
|
|
245
312
|
};
|
|
246
313
|
}
|
|
247
314
|
pause() {
|
|
248
|
-
if (this.closed) return;
|
|
315
|
+
if (this.closed || this.pausedState) return;
|
|
249
316
|
this.pausedState = true;
|
|
317
|
+
this.activeReadTimeout?.pause();
|
|
250
318
|
}
|
|
251
319
|
resume() {
|
|
252
320
|
if (!this.pausedState) return;
|
|
253
321
|
this.pausedState = false;
|
|
322
|
+
this.activeReadTimeout?.resume();
|
|
254
323
|
this.resolveResumeWaiters();
|
|
255
324
|
}
|
|
256
325
|
async close() {
|
|
@@ -258,6 +327,7 @@ var HttpFlvLoader = class {
|
|
|
258
327
|
this.state = "closed";
|
|
259
328
|
this.pausedState = false;
|
|
260
329
|
this.resolveResumeWaiters();
|
|
330
|
+
this.activeReadTimeout?.cancel();
|
|
261
331
|
this.abortController?.abort();
|
|
262
332
|
const reader = this.reader;
|
|
263
333
|
this.reader = void 0;
|
|
@@ -268,32 +338,6 @@ var HttpFlvLoader = class {
|
|
|
268
338
|
releaseReader(reader);
|
|
269
339
|
}
|
|
270
340
|
}
|
|
271
|
-
async openAttempt() {
|
|
272
|
-
const abortController = this.abortController;
|
|
273
|
-
if (abortController === void 0) throw new HttpFlvLoaderError("RIVMUX_LOADER_INVALID_STATE", "HTTP Fetch loader abort controller is missing.");
|
|
274
|
-
const response = await this.fetchImpl(this.url, {
|
|
275
|
-
method: "GET",
|
|
276
|
-
headers: createHeaders(this.headers),
|
|
277
|
-
credentials: this.credentials,
|
|
278
|
-
signal: abortController.signal
|
|
279
|
-
});
|
|
280
|
-
if (this.closed) {
|
|
281
|
-
await response.body?.cancel();
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (!response.ok) {
|
|
285
|
-
await response.body?.cancel();
|
|
286
|
-
throw new HttpFlvLoaderError("RIVMUX_HTTP_STATUS", `HTTP Fetch loader received status ${response.status} ${response.statusText}.`, response.status);
|
|
287
|
-
}
|
|
288
|
-
if (response.body === null) throw new HttpFlvLoaderError("RIVMUX_HTTP_BODY_UNAVAILABLE", "HTTP Fetch loader response body is unavailable.");
|
|
289
|
-
const contentLength = response.headers.get("Content-Length");
|
|
290
|
-
if (contentLength !== null) {
|
|
291
|
-
const parsedContentLength = Number.parseInt(contentLength, 10);
|
|
292
|
-
if (Number.isFinite(parsedContentLength) && parsedContentLength >= 0) this.mutableStats.contentLength = parsedContentLength;
|
|
293
|
-
}
|
|
294
|
-
this.reader = response.body.getReader();
|
|
295
|
-
this.state = "open";
|
|
296
|
-
}
|
|
297
341
|
waitUntilResumed() {
|
|
298
342
|
if (!this.pausedState || this.closed) return Promise.resolve();
|
|
299
343
|
return new Promise((resolve) => {
|
|
@@ -314,32 +358,198 @@ function createHeaders(headers) {
|
|
|
314
358
|
for (const [key, value] of Object.entries(headers)) result.append(key, value);
|
|
315
359
|
return result;
|
|
316
360
|
}
|
|
361
|
+
async function cancelResponseBody(response) {
|
|
362
|
+
try {
|
|
363
|
+
await response.body?.cancel();
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
317
366
|
function releaseReader(reader) {
|
|
318
367
|
try {
|
|
319
368
|
reader.releaseLock();
|
|
320
369
|
} catch {}
|
|
321
370
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
371
|
+
function createAbortError() {
|
|
372
|
+
return new DOMException("HTTP Fetch loader was aborted.", "AbortError");
|
|
373
|
+
}
|
|
374
|
+
function raceWithAbort(operation, signal, onLateValue) {
|
|
375
|
+
if (signal.aborted) {
|
|
376
|
+
operation.then(onLateValue, () => void 0);
|
|
377
|
+
return Promise.reject(createAbortError());
|
|
378
|
+
}
|
|
325
379
|
return new Promise((resolve, reject) => {
|
|
326
|
-
|
|
327
|
-
|
|
380
|
+
let settled = false;
|
|
381
|
+
const onAbort = () => {
|
|
382
|
+
if (settled) return;
|
|
383
|
+
settled = true;
|
|
328
384
|
signal.removeEventListener("abort", onAbort);
|
|
385
|
+
operation.then(onLateValue, () => void 0);
|
|
386
|
+
reject(createAbortError());
|
|
329
387
|
};
|
|
388
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
389
|
+
operation.then((value) => {
|
|
390
|
+
if (settled) return;
|
|
391
|
+
settled = true;
|
|
392
|
+
signal.removeEventListener("abort", onAbort);
|
|
393
|
+
resolve(value);
|
|
394
|
+
}, (error) => {
|
|
395
|
+
if (settled) return;
|
|
396
|
+
settled = true;
|
|
397
|
+
signal.removeEventListener("abort", onAbort);
|
|
398
|
+
reject(error);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
var ReadIdleTimeoutError = class extends Error {
|
|
403
|
+
constructor() {
|
|
404
|
+
super("HTTP Fetch loader read timed out.");
|
|
405
|
+
this.name = "ReadIdleTimeoutError";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var PausableTimeout = class {
|
|
409
|
+
promise;
|
|
410
|
+
now;
|
|
411
|
+
setTimer;
|
|
412
|
+
clearTimer;
|
|
413
|
+
reject;
|
|
414
|
+
timer;
|
|
415
|
+
remainingMs;
|
|
416
|
+
startedAtMs = 0;
|
|
417
|
+
settled = false;
|
|
418
|
+
constructor(options) {
|
|
419
|
+
this.now = options.now;
|
|
420
|
+
this.setTimer = options.setTimeout;
|
|
421
|
+
this.clearTimer = options.clearTimeout;
|
|
422
|
+
this.remainingMs = options.durationMs;
|
|
423
|
+
this.promise = new Promise((_, reject) => {
|
|
424
|
+
this.reject = reject;
|
|
425
|
+
});
|
|
426
|
+
this.start();
|
|
427
|
+
}
|
|
428
|
+
pause() {
|
|
429
|
+
if (this.settled || this.timer === void 0) return;
|
|
430
|
+
this.remainingMs = Math.max(0, this.remainingMs - Math.max(0, this.now() - this.startedAtMs));
|
|
431
|
+
this.clearTimer(this.timer);
|
|
432
|
+
this.timer = void 0;
|
|
433
|
+
}
|
|
434
|
+
resume() {
|
|
435
|
+
if (this.settled || this.timer !== void 0) return;
|
|
436
|
+
this.start();
|
|
437
|
+
}
|
|
438
|
+
cancel() {
|
|
439
|
+
if (this.settled) return;
|
|
440
|
+
this.settled = true;
|
|
441
|
+
if (this.timer !== void 0) {
|
|
442
|
+
this.clearTimer(this.timer);
|
|
443
|
+
this.timer = void 0;
|
|
444
|
+
}
|
|
445
|
+
this.reject = void 0;
|
|
446
|
+
}
|
|
447
|
+
start() {
|
|
448
|
+
this.startedAtMs = this.now();
|
|
449
|
+
this.timer = this.setTimer(() => {
|
|
450
|
+
if (this.settled) return;
|
|
451
|
+
this.settled = true;
|
|
452
|
+
this.timer = void 0;
|
|
453
|
+
this.reject?.(new ReadIdleTimeoutError());
|
|
454
|
+
this.reject = void 0;
|
|
455
|
+
}, this.remainingMs);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/loader/retry-policy.ts
|
|
460
|
+
/**
|
|
461
|
+
* Calculates the delay after a failed one-based connection attempt.
|
|
462
|
+
* Attempt 1 is the initial connection, so its retry uses the base delay.
|
|
463
|
+
*/
|
|
464
|
+
function getRetryDelayMs(policy, failedAttempt, random = Math.random) {
|
|
465
|
+
if (policy.backoffMs === 0 || policy.maxBackoffMs === 0) return 0;
|
|
466
|
+
const exponent = Math.max(0, Math.trunc(failedAttempt) - 1);
|
|
467
|
+
const exponentialDelay = Math.min(policy.maxBackoffMs, policy.backoffMs * 2 ** exponent);
|
|
468
|
+
const jitterMultiplier = 1 + (normalizeRandom(random()) * 2 - 1) * policy.jitterRatio;
|
|
469
|
+
return Math.min(policy.maxBackoffMs, Math.max(0, Math.round(exponentialDelay * jitterMultiplier)));
|
|
470
|
+
}
|
|
471
|
+
function isRecoverableHttpStatus(status) {
|
|
472
|
+
return status === 408 || status === 429 || status >= 500 && status <= 599;
|
|
473
|
+
}
|
|
474
|
+
function isRecoverableLoaderError(cause) {
|
|
475
|
+
if (!(cause instanceof HttpFlvLoaderError) || cause.reason === void 0) return false;
|
|
476
|
+
if (cause.reason === "http-status") return cause.status !== void 0 && isRecoverableHttpStatus(cause.status);
|
|
477
|
+
return cause.reason === "network-error" || cause.reason === "read-error" || cause.reason === "read-timeout" || cause.reason === "unexpected-eof";
|
|
478
|
+
}
|
|
479
|
+
function normalizeRandom(value) {
|
|
480
|
+
if (!Number.isFinite(value)) return .5;
|
|
481
|
+
return Math.min(1, Math.max(0, value));
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/runtime/lifecycle.ts
|
|
485
|
+
function raceLifecycleOperation(operation, signal, onLateValue) {
|
|
486
|
+
if (signal.aborted) {
|
|
487
|
+
operation.then(onLateValue, () => void 0);
|
|
488
|
+
return Promise.resolve({ cancelled: true });
|
|
489
|
+
}
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
let settled = false;
|
|
330
492
|
const onAbort = () => {
|
|
331
|
-
|
|
332
|
-
|
|
493
|
+
if (settled) return;
|
|
494
|
+
settled = true;
|
|
495
|
+
signal.removeEventListener("abort", onAbort);
|
|
496
|
+
operation.then(onLateValue, () => void 0);
|
|
497
|
+
resolve({ cancelled: true });
|
|
333
498
|
};
|
|
334
|
-
const timer = setTimeout(() => {
|
|
335
|
-
cleanup();
|
|
336
|
-
resolve();
|
|
337
|
-
}, ms);
|
|
338
499
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
500
|
+
operation.then((value) => {
|
|
501
|
+
if (settled) return;
|
|
502
|
+
settled = true;
|
|
503
|
+
signal.removeEventListener("abort", onAbort);
|
|
504
|
+
resolve({
|
|
505
|
+
cancelled: false,
|
|
506
|
+
value
|
|
507
|
+
});
|
|
508
|
+
}, (error) => {
|
|
509
|
+
if (settled) return;
|
|
510
|
+
settled = true;
|
|
511
|
+
signal.removeEventListener("abort", onAbort);
|
|
512
|
+
reject(error);
|
|
513
|
+
});
|
|
339
514
|
});
|
|
340
515
|
}
|
|
341
|
-
|
|
342
|
-
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region src/runtime/stats.ts
|
|
518
|
+
function updateAppendQueueHighWaterMark(current, mseStats) {
|
|
519
|
+
return {
|
|
520
|
+
length: Math.max(current.length, mseStats.appendQueueLength),
|
|
521
|
+
bytes: Math.max(current.bytes, mseStats.appendQueueBytes)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function getNetworkIdleMs(stats, nowMs) {
|
|
525
|
+
const markerMs = stats?.lastChunkAtMs ?? stats?.startedAtMs;
|
|
526
|
+
if (markerMs === void 0) return;
|
|
527
|
+
return Math.max(nowMs - markerMs, 0);
|
|
528
|
+
}
|
|
529
|
+
function createPlayerStats(snapshot) {
|
|
530
|
+
const { loaderStats, mseStats, latencyMetrics } = snapshot;
|
|
531
|
+
return {
|
|
532
|
+
bytesReceived: loaderStats?.bytesReceived ?? 0,
|
|
533
|
+
currentNetworkSpeed: loaderStats?.currentNetworkSpeed ?? 0,
|
|
534
|
+
networkIdleMs: getNetworkIdleMs(loaderStats, snapshot.nowMs),
|
|
535
|
+
outputBytes: snapshot.outputBytes,
|
|
536
|
+
appendQueueLength: mseStats.appendQueueLength,
|
|
537
|
+
appendQueueBytes: mseStats.appendQueueBytes,
|
|
538
|
+
appendQueueMaxLength: snapshot.appendQueueMaxLength,
|
|
539
|
+
appendQueueMaxBytes: snapshot.appendQueueMaxBytes,
|
|
540
|
+
loaderPaused: snapshot.loaderPaused,
|
|
541
|
+
sourceBufferUpdating: mseStats.sourceBufferUpdating,
|
|
542
|
+
sourceBufferCount: mseStats.sourceBufferCount,
|
|
543
|
+
bufferedStart: latencyMetrics.bufferedStart ?? mseStats.bufferedStart,
|
|
544
|
+
bufferedEnd: latencyMetrics.bufferedEnd ?? mseStats.bufferedEnd,
|
|
545
|
+
bufferedDuration: latencyMetrics.bufferedDuration ?? mseStats.bufferedDuration,
|
|
546
|
+
bufferedRangeCount: mseStats.bufferedRangeCount,
|
|
547
|
+
currentTime: latencyMetrics.currentTime,
|
|
548
|
+
liveLatency: latencyMetrics.liveLatency,
|
|
549
|
+
playbackRate: latencyMetrics.playbackRate,
|
|
550
|
+
readyState: latencyMetrics.readyState,
|
|
551
|
+
droppedFrames: latencyMetrics.droppedFrames
|
|
552
|
+
};
|
|
343
553
|
}
|
|
344
554
|
//#endregion
|
|
345
555
|
//#region src/mse/mime.ts
|
|
@@ -349,25 +559,25 @@ function createMp4VideoMime(codec) {
|
|
|
349
559
|
function createMp4AudioMime(codec) {
|
|
350
560
|
return `audio/mp4; codecs="${codec}"`;
|
|
351
561
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
mimeType
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}];
|
|
562
|
+
var MseUnsupportedMimeError = class extends Error {
|
|
563
|
+
mimeType;
|
|
564
|
+
constructor(mimeType) {
|
|
565
|
+
super(`MSE does not support ${mimeType}.`);
|
|
566
|
+
this.name = "MseUnsupportedMimeError";
|
|
567
|
+
this.mimeType = mimeType;
|
|
568
|
+
}
|
|
569
|
+
};
|
|
361
570
|
function isMseSupported(mimeType) {
|
|
362
571
|
return typeof MediaSource !== "undefined" && typeof MediaSource.isTypeSupported === "function" && MediaSource.isTypeSupported(mimeType);
|
|
363
572
|
}
|
|
364
|
-
function
|
|
573
|
+
function assertMseRuntimeSupport() {
|
|
365
574
|
if (typeof MediaSource === "undefined") throw new Error("MediaSource is not available in this worker.");
|
|
366
575
|
if (MediaSource.canConstructInDedicatedWorker !== true) throw new Error("MediaSource cannot be constructed in this dedicated worker.");
|
|
367
|
-
if (
|
|
576
|
+
if (typeof MediaSource.isTypeSupported !== "function") throw new Error("MediaSource.isTypeSupported is not available in this worker.");
|
|
368
577
|
}
|
|
369
|
-
function
|
|
370
|
-
|
|
578
|
+
function assertMseSupport(mimeType) {
|
|
579
|
+
assertMseRuntimeSupport();
|
|
580
|
+
if (!isMseSupported(mimeType)) throw new MseUnsupportedMimeError(mimeType);
|
|
371
581
|
}
|
|
372
582
|
//#endregion
|
|
373
583
|
//#region src/mse/source-buffer-queue.ts
|
|
@@ -518,7 +728,7 @@ var MseController = class {
|
|
|
518
728
|
return Array.from(this.queues.values()).reduce((total, queue) => total + queue.bufferedRanges.length, 0);
|
|
519
729
|
}
|
|
520
730
|
async createMediaSourceHandle() {
|
|
521
|
-
|
|
731
|
+
assertMseRuntimeSupport();
|
|
522
732
|
const mediaSource = new MediaSource();
|
|
523
733
|
this.mediaSource = mediaSource;
|
|
524
734
|
const handle = mediaSource.handle;
|
|
@@ -533,7 +743,7 @@ var MseController = class {
|
|
|
533
743
|
await this.ensureQueue(segment.track, mimeType).append(toAppendBuffer(segment.bytes));
|
|
534
744
|
}
|
|
535
745
|
async appendMediaSegment(segment) {
|
|
536
|
-
const queue = this.queues.get(segment.track);
|
|
746
|
+
const queue = this.queues.get(segment.track) ?? this.queues.get("muxed");
|
|
537
747
|
if (queue === void 0) throw new Error(`Cannot append ${segment.track} media segment before init segment.`);
|
|
538
748
|
await queue.append(toAppendBuffer(segment.bytes));
|
|
539
749
|
const mediaSource = this.requireMediaSource();
|
|
@@ -677,10 +887,8 @@ function normalizeCoreEvent(value) {
|
|
|
677
887
|
type: "mediaSegment",
|
|
678
888
|
data: normalizeMediaSegment(data)
|
|
679
889
|
};
|
|
680
|
-
case "
|
|
681
|
-
case "
|
|
682
|
-
case "videoSample":
|
|
683
|
-
case "audioSample":
|
|
890
|
+
case "trackConfig":
|
|
891
|
+
case "sample":
|
|
684
892
|
case "metadata":
|
|
685
893
|
case "discontinuity": return {
|
|
686
894
|
type: value.type,
|
|
@@ -840,16 +1048,9 @@ var TransmuxCore = class {
|
|
|
840
1048
|
* @returns {any}
|
|
841
1049
|
*/
|
|
842
1050
|
flush() {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
|
|
847
|
-
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
|
|
848
|
-
if (getDataViewMemory0().getInt32(retptr + 8, true)) throw takeObject(r1);
|
|
849
|
-
return takeObject(r0);
|
|
850
|
-
} finally {
|
|
851
|
-
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
852
|
-
}
|
|
1051
|
+
const ret = wasm.transmuxcore_flush(this.__wbg_ptr);
|
|
1052
|
+
if (ret[2]) throw takeFromExternrefTable0(ret[1]);
|
|
1053
|
+
return takeFromExternrefTable0(ret[0]);
|
|
853
1054
|
}
|
|
854
1055
|
constructor() {
|
|
855
1056
|
const ret = wasm.transmuxcore_new();
|
|
@@ -862,18 +1063,11 @@ var TransmuxCore = class {
|
|
|
862
1063
|
* @returns {any}
|
|
863
1064
|
*/
|
|
864
1065
|
pushChunk(data) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
var r0 = getDataViewMemory0().getInt32(retptr + 0, true);
|
|
871
|
-
var r1 = getDataViewMemory0().getInt32(retptr + 4, true);
|
|
872
|
-
if (getDataViewMemory0().getInt32(retptr + 8, true)) throw takeObject(r1);
|
|
873
|
-
return takeObject(r0);
|
|
874
|
-
} finally {
|
|
875
|
-
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
876
|
-
}
|
|
1066
|
+
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
|
|
1067
|
+
const len0 = WASM_VECTOR_LEN;
|
|
1068
|
+
const ret = wasm.transmuxcore_pushChunk(this.__wbg_ptr, ptr0, len0);
|
|
1069
|
+
if (ret[2]) throw takeFromExternrefTable0(ret[1]);
|
|
1070
|
+
return takeFromExternrefTable0(ret[0]);
|
|
877
1071
|
}
|
|
878
1072
|
reset() {
|
|
879
1073
|
wasm.transmuxcore_reset(this.__wbg_ptr);
|
|
@@ -885,44 +1079,50 @@ function __wbg_get_imports() {
|
|
|
885
1079
|
__proto__: null,
|
|
886
1080
|
"./rivmux_transmux_core_bg.js": {
|
|
887
1081
|
__proto__: null,
|
|
888
|
-
|
|
889
|
-
return
|
|
1082
|
+
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
|
1083
|
+
return Error(getStringFromWasm0(arg0, arg1));
|
|
1084
|
+
},
|
|
1085
|
+
__wbg_Number_c54e7112a3fa7e3e: function(arg0) {
|
|
1086
|
+
return Number(arg0);
|
|
890
1087
|
},
|
|
891
1088
|
__wbg_String_8564e559799eccda: function(arg0, arg1) {
|
|
892
|
-
const ptr1 = passStringToWasm0(String(
|
|
1089
|
+
const ptr1 = passStringToWasm0(String(arg1), wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
893
1090
|
const len1 = WASM_VECTOR_LEN;
|
|
894
1091
|
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
|
|
895
1092
|
getDataViewMemory0().setInt32(arg0 + 0, ptr1, true);
|
|
896
1093
|
},
|
|
897
|
-
|
|
1094
|
+
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
|
898
1095
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
899
1096
|
},
|
|
900
|
-
|
|
901
|
-
return
|
|
902
|
-
},
|
|
903
|
-
__wbg_new_36e147a8ced3c6e0: function() {
|
|
904
|
-
return addHeapObject(new Array());
|
|
1097
|
+
__wbg_new_bebc3f4757acf305: function() {
|
|
1098
|
+
return /* @__PURE__ */ new Object();
|
|
905
1099
|
},
|
|
906
|
-
|
|
907
|
-
|
|
1100
|
+
__wbg_new_ffa92086ea89f79c: function() {
|
|
1101
|
+
return new Array();
|
|
908
1102
|
},
|
|
909
|
-
|
|
910
|
-
|
|
1103
|
+
__wbg_set_13d25b81ab403f5e: function(arg0, arg1, arg2) {
|
|
1104
|
+
arg0[arg1 >>> 0] = arg2;
|
|
911
1105
|
},
|
|
912
|
-
|
|
913
|
-
|
|
1106
|
+
__wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
|
|
1107
|
+
arg0[arg1] = arg2;
|
|
914
1108
|
},
|
|
915
|
-
|
|
916
|
-
return
|
|
1109
|
+
__wbindgen_generic_0000000000000001: function(arg0) {
|
|
1110
|
+
return arg0;
|
|
917
1111
|
},
|
|
918
|
-
|
|
919
|
-
return
|
|
1112
|
+
__wbindgen_generic_0000000000000002: function(arg0) {
|
|
1113
|
+
return arg0;
|
|
920
1114
|
},
|
|
921
|
-
|
|
922
|
-
return
|
|
1115
|
+
__wbindgen_generic_0000000000000003: function(arg0, arg1) {
|
|
1116
|
+
return getStringFromWasm0(arg0, arg1);
|
|
923
1117
|
},
|
|
924
|
-
|
|
925
|
-
|
|
1118
|
+
__wbindgen_init_externref_table: function() {
|
|
1119
|
+
const table = wasm.__wbindgen_externrefs;
|
|
1120
|
+
const offset = table.grow(4);
|
|
1121
|
+
table.set(0, void 0);
|
|
1122
|
+
table.set(offset + 0, void 0);
|
|
1123
|
+
table.set(offset + 1, null);
|
|
1124
|
+
table.set(offset + 2, true);
|
|
1125
|
+
table.set(offset + 3, false);
|
|
926
1126
|
}
|
|
927
1127
|
}
|
|
928
1128
|
};
|
|
@@ -931,18 +1131,6 @@ const TransmuxCoreFinalization = typeof FinalizationRegistry === "undefined" ? {
|
|
|
931
1131
|
register: () => {},
|
|
932
1132
|
unregister: () => {}
|
|
933
1133
|
} : new FinalizationRegistry((ptr) => wasm.__wbg_transmuxcore_free(ptr, 1));
|
|
934
|
-
function addHeapObject(obj) {
|
|
935
|
-
if (heap_next === heap.length) heap.push(heap.length + 1);
|
|
936
|
-
const idx = heap_next;
|
|
937
|
-
heap_next = heap[idx];
|
|
938
|
-
heap[idx] = obj;
|
|
939
|
-
return idx;
|
|
940
|
-
}
|
|
941
|
-
function dropObject(idx) {
|
|
942
|
-
if (idx < 1028) return;
|
|
943
|
-
heap[idx] = heap_next;
|
|
944
|
-
heap_next = idx;
|
|
945
|
-
}
|
|
946
1134
|
let cachedDataViewMemory0 = null;
|
|
947
1135
|
function getDataViewMemory0() {
|
|
948
1136
|
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || cachedDataViewMemory0.buffer.detached === void 0 && cachedDataViewMemory0.buffer !== wasm.memory.buffer) cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
@@ -956,12 +1144,6 @@ function getUint8ArrayMemory0() {
|
|
|
956
1144
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
957
1145
|
return cachedUint8ArrayMemory0;
|
|
958
1146
|
}
|
|
959
|
-
function getObject(idx) {
|
|
960
|
-
return heap[idx];
|
|
961
|
-
}
|
|
962
|
-
let heap = new Array(1024).fill(void 0);
|
|
963
|
-
heap.push(void 0, null, true, false);
|
|
964
|
-
let heap_next = heap.length;
|
|
965
1147
|
function passArray8ToWasm0(arg, malloc) {
|
|
966
1148
|
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
967
1149
|
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
@@ -996,10 +1178,10 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|
|
996
1178
|
WASM_VECTOR_LEN = offset;
|
|
997
1179
|
return ptr;
|
|
998
1180
|
}
|
|
999
|
-
function
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
return
|
|
1181
|
+
function takeFromExternrefTable0(idx) {
|
|
1182
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
1183
|
+
wasm.__externref_table_dealloc(idx);
|
|
1184
|
+
return value;
|
|
1003
1185
|
}
|
|
1004
1186
|
let cachedTextDecoder = new TextDecoder("utf-8", {
|
|
1005
1187
|
ignoreBOM: true,
|
|
@@ -1035,14 +1217,16 @@ function __wbg_finalize_init(instance, module) {
|
|
|
1035
1217
|
wasm = instance.exports;
|
|
1036
1218
|
cachedDataViewMemory0 = null;
|
|
1037
1219
|
cachedUint8ArrayMemory0 = null;
|
|
1220
|
+
wasm.__wbindgen_start();
|
|
1038
1221
|
return wasm;
|
|
1039
1222
|
}
|
|
1040
1223
|
async function __wbg_load(module, imports) {
|
|
1041
1224
|
if (typeof Response === "function" && module instanceof Response) {
|
|
1225
|
+
if (!module.ok) throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
|
1042
1226
|
if (typeof WebAssembly.instantiateStreaming === "function") try {
|
|
1043
1227
|
return await WebAssembly.instantiateStreaming(module, imports);
|
|
1044
1228
|
} catch (e) {
|
|
1045
|
-
if (
|
|
1229
|
+
if (expectedResponseType(module.type) && module.headers.get("Content-Type") !== "application/wasm") console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
|
1046
1230
|
else throw e;
|
|
1047
1231
|
}
|
|
1048
1232
|
const bytes = await module.arrayBuffer();
|
|
@@ -1066,8 +1250,10 @@ async function __wbg_load(module, imports) {
|
|
|
1066
1250
|
}
|
|
1067
1251
|
async function __wbg_init(module_or_path) {
|
|
1068
1252
|
if (wasm !== void 0) return wasm;
|
|
1069
|
-
if (module_or_path !== void 0)
|
|
1070
|
-
|
|
1253
|
+
if (module_or_path !== void 0) {
|
|
1254
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) ({module_or_path} = module_or_path);
|
|
1255
|
+
else console.warn("using deprecated parameters for the initialization function; pass a single object instead");
|
|
1256
|
+
}
|
|
1071
1257
|
if (module_or_path === void 0) module_or_path = new URL("rivmux_transmux_core_bg.wasm", import.meta.url);
|
|
1072
1258
|
const imports = __wbg_get_imports();
|
|
1073
1259
|
if (typeof module_or_path === "string" || typeof Request === "function" && module_or_path instanceof Request || typeof URL === "function" && module_or_path instanceof URL) module_or_path = fetch(module_or_path);
|
|
@@ -1084,41 +1270,494 @@ async function loadWasmTransmuxCoreHost(wasmUrl) {
|
|
|
1084
1270
|
await __wbg_init(wasmUrl ?? new URL("./rivmux-transmux-core.wasm", import.meta.url));
|
|
1085
1271
|
return createWasmTransmuxCoreHost(TransmuxCore);
|
|
1086
1272
|
}
|
|
1273
|
+
var Fmp4AppendBatcher = class {
|
|
1274
|
+
pending = /* @__PURE__ */ new Map();
|
|
1275
|
+
timers = /* @__PURE__ */ new Map();
|
|
1276
|
+
maxDurationMs;
|
|
1277
|
+
maxBytes;
|
|
1278
|
+
onFlushDue;
|
|
1279
|
+
constructor(onFlushDue, options = {}) {
|
|
1280
|
+
this.onFlushDue = onFlushDue;
|
|
1281
|
+
this.maxDurationMs = options.maxDurationMs ?? 125;
|
|
1282
|
+
this.maxBytes = options.maxBytes ?? 524288;
|
|
1283
|
+
}
|
|
1284
|
+
push(segment) {
|
|
1285
|
+
let batch = this.pending.get(segment.track);
|
|
1286
|
+
if (batch !== void 0 && this.wouldExceedLimit(batch, segment)) {
|
|
1287
|
+
const flushed = this.flush(segment.track);
|
|
1288
|
+
batch = this.createBatch(segment);
|
|
1289
|
+
this.pending.set(segment.track, batch);
|
|
1290
|
+
this.scheduleFlush(segment.track);
|
|
1291
|
+
return flushed;
|
|
1292
|
+
}
|
|
1293
|
+
if (batch === void 0) {
|
|
1294
|
+
batch = this.createBatch(segment);
|
|
1295
|
+
this.pending.set(segment.track, batch);
|
|
1296
|
+
this.scheduleFlush(segment.track);
|
|
1297
|
+
} else {
|
|
1298
|
+
batch.dtsEndMs = Math.max(batch.dtsEndMs, segment.dtsEndMs);
|
|
1299
|
+
batch.parts.push(segment.bytes);
|
|
1300
|
+
batch.byteLength += segment.bytes.byteLength;
|
|
1301
|
+
}
|
|
1302
|
+
return batch.byteLength >= this.maxBytes ? this.flush(segment.track) : void 0;
|
|
1303
|
+
}
|
|
1304
|
+
flush(track) {
|
|
1305
|
+
const batch = this.pending.get(track);
|
|
1306
|
+
if (batch === void 0) return;
|
|
1307
|
+
this.pending.delete(track);
|
|
1308
|
+
this.cancelFlush(track);
|
|
1309
|
+
return {
|
|
1310
|
+
track,
|
|
1311
|
+
dtsStartMs: batch.dtsStartMs,
|
|
1312
|
+
dtsEndMs: batch.dtsEndMs,
|
|
1313
|
+
keyframe: batch.keyframe,
|
|
1314
|
+
bytes: mergeBytes(batch.parts, batch.byteLength)
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
flushAll() {
|
|
1318
|
+
const batches = [];
|
|
1319
|
+
for (const track of [...this.pending.keys()]) {
|
|
1320
|
+
const batch = this.flush(track);
|
|
1321
|
+
if (batch !== void 0) batches.push(batch);
|
|
1322
|
+
}
|
|
1323
|
+
return batches;
|
|
1324
|
+
}
|
|
1325
|
+
discard() {
|
|
1326
|
+
this.pending.clear();
|
|
1327
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
1328
|
+
this.timers.clear();
|
|
1329
|
+
}
|
|
1330
|
+
createBatch(segment) {
|
|
1331
|
+
return {
|
|
1332
|
+
track: segment.track,
|
|
1333
|
+
dtsStartMs: segment.dtsStartMs,
|
|
1334
|
+
dtsEndMs: segment.dtsEndMs,
|
|
1335
|
+
keyframe: segment.keyframe,
|
|
1336
|
+
parts: [segment.bytes],
|
|
1337
|
+
byteLength: segment.bytes.byteLength
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
wouldExceedLimit(batch, segment) {
|
|
1341
|
+
return Math.max(batch.dtsEndMs, segment.dtsEndMs) - batch.dtsStartMs > this.maxDurationMs || batch.byteLength + segment.bytes.byteLength > this.maxBytes;
|
|
1342
|
+
}
|
|
1343
|
+
scheduleFlush(track) {
|
|
1344
|
+
this.timers.set(track, setTimeout(() => {
|
|
1345
|
+
this.timers.delete(track);
|
|
1346
|
+
this.onFlushDue(track);
|
|
1347
|
+
}, this.maxDurationMs));
|
|
1348
|
+
}
|
|
1349
|
+
cancelFlush(track) {
|
|
1350
|
+
const timer = this.timers.get(track);
|
|
1351
|
+
if (timer !== void 0) {
|
|
1352
|
+
clearTimeout(timer);
|
|
1353
|
+
this.timers.delete(track);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
function mergeBytes(parts, byteLength) {
|
|
1358
|
+
const bytes = new Uint8Array(byteLength);
|
|
1359
|
+
let offset = 0;
|
|
1360
|
+
for (const part of parts) {
|
|
1361
|
+
bytes.set(part, offset);
|
|
1362
|
+
offset += part.byteLength;
|
|
1363
|
+
}
|
|
1364
|
+
return bytes;
|
|
1365
|
+
}
|
|
1087
1366
|
//#endregion
|
|
1088
|
-
//#region src/runtime.ts
|
|
1089
|
-
|
|
1090
|
-
|
|
1367
|
+
//#region src/runtime/append.ts
|
|
1368
|
+
/** Owns fMP4 batching and serialized MSE media-segment appends. */
|
|
1369
|
+
var RuntimeAppendController = class {
|
|
1370
|
+
dependencies;
|
|
1371
|
+
batcher;
|
|
1372
|
+
generation = 0;
|
|
1373
|
+
tail = Promise.resolve(true);
|
|
1374
|
+
constructor(dependencies) {
|
|
1375
|
+
this.dependencies = dependencies;
|
|
1376
|
+
}
|
|
1377
|
+
start(context) {
|
|
1378
|
+
this.discard();
|
|
1379
|
+
this.batcher = new Fmp4AppendBatcher((track) => {
|
|
1380
|
+
const batch = this.batcher?.flush(track);
|
|
1381
|
+
if (batch !== void 0) this.enqueue(batch, context);
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
push(segment, context) {
|
|
1385
|
+
const batch = this.batcher?.push(segment);
|
|
1386
|
+
return batch === void 0 ? void 0 : this.enqueue(batch, context);
|
|
1387
|
+
}
|
|
1388
|
+
async flush(context, track) {
|
|
1389
|
+
const batcher = this.batcher;
|
|
1390
|
+
if (batcher === void 0) return true;
|
|
1391
|
+
const batches = track === void 0 ? batcher.flushAll() : [batcher.flush(track)].filter((batch) => batch !== void 0);
|
|
1392
|
+
for (const batch of batches) if (!await this.enqueue(batch, context)) return false;
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
async waitForTail() {
|
|
1396
|
+
return this.tail;
|
|
1397
|
+
}
|
|
1398
|
+
discard() {
|
|
1399
|
+
this.generation += 1;
|
|
1400
|
+
this.batcher?.discard();
|
|
1401
|
+
this.batcher = void 0;
|
|
1402
|
+
this.tail = Promise.resolve(true);
|
|
1403
|
+
}
|
|
1404
|
+
enqueue(segment, context) {
|
|
1405
|
+
const generation = this.generation;
|
|
1406
|
+
const append = this.tail.then(async (previousAppendSucceeded) => {
|
|
1407
|
+
if (!previousAppendSucceeded || generation !== this.generation || !this.dependencies.isStarted() || !this.dependencies.isLifecycleContextCurrent(context)) return false;
|
|
1408
|
+
if (!await this.dependencies.appendToMse(segment, context)) return false;
|
|
1409
|
+
if (!this.dependencies.isLifecycleContextCurrent(context)) return false;
|
|
1410
|
+
await this.dependencies.onAppended(segment, context);
|
|
1411
|
+
return true;
|
|
1412
|
+
});
|
|
1413
|
+
this.tail = append.catch((cause) => {
|
|
1414
|
+
if (this.dependencies.isLifecycleContextCurrent(context)) this.dependencies.onError(cause, context);
|
|
1415
|
+
return false;
|
|
1416
|
+
});
|
|
1417
|
+
return this.tail;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/runtime/session.ts
|
|
1422
|
+
/** Owns all resources that exist only for an attached playback session. */
|
|
1423
|
+
var RuntimeSession = class {
|
|
1424
|
+
dependencies;
|
|
1091
1425
|
createMseController;
|
|
1092
1426
|
createLoader;
|
|
1093
1427
|
createTransmuxCore;
|
|
1428
|
+
isStarted;
|
|
1429
|
+
isLifecycleContextCurrent;
|
|
1430
|
+
mse;
|
|
1431
|
+
loader;
|
|
1432
|
+
transmuxCore;
|
|
1433
|
+
loaderRunId = 0;
|
|
1434
|
+
loaderClosePromise;
|
|
1435
|
+
outputBytes = 0;
|
|
1436
|
+
appendController;
|
|
1437
|
+
constructor(dependencies) {
|
|
1438
|
+
this.dependencies = dependencies;
|
|
1439
|
+
this.createMseController = dependencies.createMseController ?? (() => new MseController());
|
|
1440
|
+
this.createLoader = dependencies.createLoader ?? ((config) => new HttpFlvLoader(config));
|
|
1441
|
+
this.createTransmuxCore = dependencies.createTransmuxCore ?? ((options) => loadWasmTransmuxCoreHost(options.runtime.wasmUrl));
|
|
1442
|
+
this.isStarted = dependencies.isStarted;
|
|
1443
|
+
this.isLifecycleContextCurrent = dependencies.isLifecycleContextCurrent;
|
|
1444
|
+
this.appendController = new RuntimeAppendController({
|
|
1445
|
+
appendToMse: (segment, context) => this.appendToMse(context, () => this.mse?.appendMediaSegment(segment)),
|
|
1446
|
+
isStarted: dependencies.isStarted,
|
|
1447
|
+
isLifecycleContextCurrent: dependencies.isLifecycleContextCurrent,
|
|
1448
|
+
onAppended: async (segment, context) => {
|
|
1449
|
+
if (!this.isLifecycleContextCurrent(context)) return;
|
|
1450
|
+
this.outputBytes += segment.bytes.byteLength;
|
|
1451
|
+
await dependencies.onMediaAppended(context);
|
|
1452
|
+
},
|
|
1453
|
+
onError: dependencies.onAppendError
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
get hasMse() {
|
|
1457
|
+
return this.mse !== void 0;
|
|
1458
|
+
}
|
|
1459
|
+
get loaderStats() {
|
|
1460
|
+
return this.loader?.stats;
|
|
1461
|
+
}
|
|
1462
|
+
get loaderPaused() {
|
|
1463
|
+
return this.loader?.paused ?? false;
|
|
1464
|
+
}
|
|
1465
|
+
get bufferedRanges() {
|
|
1466
|
+
return this.mse?.bufferedRanges ?? [];
|
|
1467
|
+
}
|
|
1468
|
+
get emittedBytes() {
|
|
1469
|
+
return this.outputBytes;
|
|
1470
|
+
}
|
|
1471
|
+
async attach(context) {
|
|
1472
|
+
this.mse ??= this.createMseController();
|
|
1473
|
+
const attachment = await raceLifecycleOperation(this.mse.createMediaSourceHandle(), context.signal);
|
|
1474
|
+
if (attachment.cancelled || !this.isLifecycleContextCurrent(context)) return;
|
|
1475
|
+
return attachment.value;
|
|
1476
|
+
}
|
|
1477
|
+
async createCore(options, context) {
|
|
1478
|
+
const creation = await raceLifecycleOperation(Promise.resolve(this.createTransmuxCore(options)), context.signal, (lateCore) => lateCore?.destroy());
|
|
1479
|
+
if (creation.cancelled || !this.isLifecycleContextCurrent(context)) return;
|
|
1480
|
+
return creation.value;
|
|
1481
|
+
}
|
|
1482
|
+
start(core, context) {
|
|
1483
|
+
this.transmuxCore?.destroy();
|
|
1484
|
+
this.transmuxCore = core;
|
|
1485
|
+
this.outputBytes = 0;
|
|
1486
|
+
this.appendController.start(context);
|
|
1487
|
+
}
|
|
1488
|
+
runLoader(config, context) {
|
|
1489
|
+
const { loader, runId } = this.startLoader(config);
|
|
1490
|
+
this.consumeLoader(loader, runId, context);
|
|
1491
|
+
}
|
|
1492
|
+
async close() {
|
|
1493
|
+
this.dependencies.onLoaderClosing();
|
|
1494
|
+
this.appendController.discard();
|
|
1495
|
+
this.transmuxCore?.destroy();
|
|
1496
|
+
this.transmuxCore = void 0;
|
|
1497
|
+
const loader = this.loader;
|
|
1498
|
+
if (loader === void 0) {
|
|
1499
|
+
await this.loaderClosePromise;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
this.loader = void 0;
|
|
1503
|
+
this.loaderRunId += 1;
|
|
1504
|
+
await this.closeLoaderInstance(loader);
|
|
1505
|
+
}
|
|
1506
|
+
destroyMse() {
|
|
1507
|
+
this.mse?.destroy();
|
|
1508
|
+
this.mse = void 0;
|
|
1509
|
+
}
|
|
1510
|
+
discardAppend() {
|
|
1511
|
+
this.appendController.discard();
|
|
1512
|
+
}
|
|
1513
|
+
collectMseStats() {
|
|
1514
|
+
return {
|
|
1515
|
+
appendQueueLength: this.mse?.appendQueueLength ?? 0,
|
|
1516
|
+
appendQueueBytes: this.mse?.appendQueueBytes ?? 0,
|
|
1517
|
+
sourceBufferUpdating: this.mse?.sourceBufferUpdating ?? false,
|
|
1518
|
+
sourceBufferCount: this.mse?.sourceBufferCount ?? 0,
|
|
1519
|
+
bufferedRangeCount: this.mse?.bufferedRangeCount ?? 0,
|
|
1520
|
+
bufferedStart: this.mse?.bufferedStart,
|
|
1521
|
+
bufferedEnd: this.mse?.bufferedEnd,
|
|
1522
|
+
bufferedDuration: this.mse?.bufferedDuration
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
cleanupBefore(cutoff, force = false) {
|
|
1526
|
+
return this.mse?.cleanupBefore(cutoff, force ? { force: true } : void 0);
|
|
1527
|
+
}
|
|
1528
|
+
pauseLoader() {
|
|
1529
|
+
this.loader?.pause();
|
|
1530
|
+
}
|
|
1531
|
+
resumeLoader() {
|
|
1532
|
+
this.loader?.resume();
|
|
1533
|
+
}
|
|
1534
|
+
startLoader(config) {
|
|
1535
|
+
const loader = this.createLoader(config);
|
|
1536
|
+
const runId = this.loaderRunId + 1;
|
|
1537
|
+
this.loaderRunId = runId;
|
|
1538
|
+
this.loader = loader;
|
|
1539
|
+
return {
|
|
1540
|
+
loader,
|
|
1541
|
+
runId
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
pushChunk(bytes) {
|
|
1545
|
+
return this.transmuxCore?.pushChunk(bytes) ?? [];
|
|
1546
|
+
}
|
|
1547
|
+
isCurrentLoader(loader, runId) {
|
|
1548
|
+
return this.loader === loader && this.loaderRunId === runId && this.isStarted();
|
|
1549
|
+
}
|
|
1550
|
+
async closeCurrentLoader(loader, runId) {
|
|
1551
|
+
if (this.loader !== loader || this.loaderRunId !== runId) return;
|
|
1552
|
+
this.dependencies.onLoaderClosing();
|
|
1553
|
+
this.appendController.discard();
|
|
1554
|
+
this.loader = void 0;
|
|
1555
|
+
this.loaderRunId += 1;
|
|
1556
|
+
this.transmuxCore?.destroy();
|
|
1557
|
+
this.transmuxCore = void 0;
|
|
1558
|
+
await this.closeLoaderInstance(loader);
|
|
1559
|
+
}
|
|
1560
|
+
async closeLoaderInstance(loader) {
|
|
1561
|
+
let closing;
|
|
1562
|
+
try {
|
|
1563
|
+
closing = loader.close();
|
|
1564
|
+
} catch (cause) {
|
|
1565
|
+
closing = Promise.reject(cause);
|
|
1566
|
+
}
|
|
1567
|
+
this.loaderClosePromise = closing;
|
|
1568
|
+
try {
|
|
1569
|
+
await closing;
|
|
1570
|
+
} finally {
|
|
1571
|
+
if (this.loaderClosePromise === closing) this.loaderClosePromise = void 0;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
async consumeLoader(loader, runId, context) {
|
|
1575
|
+
try {
|
|
1576
|
+
await loader.open();
|
|
1577
|
+
while (this.isCurrentLoader(loader, runId) && this.isLifecycleContextCurrent(context)) {
|
|
1578
|
+
await this.dependencies.applyLatencyPolicy(context);
|
|
1579
|
+
if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
|
|
1580
|
+
const chunk = await loader.read();
|
|
1581
|
+
if (chunk === null) {
|
|
1582
|
+
if (this.isCurrentLoader(loader, runId) && this.isLifecycleContextCurrent(context)) {
|
|
1583
|
+
if (!await this.appendController.flush(context)) return;
|
|
1584
|
+
this.dependencies.onStats(loader.stats);
|
|
1585
|
+
}
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
|
|
1589
|
+
this.dependencies.onStats(loader.stats);
|
|
1590
|
+
if (!await this.processEvents(this.pushChunk(chunk.bytes), context)) {
|
|
1591
|
+
await this.closeCurrentLoader(loader, runId);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
await this.dependencies.applyLatencyPolicy(context);
|
|
1595
|
+
if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
|
|
1596
|
+
this.dependencies.onStats(loader.stats);
|
|
1597
|
+
}
|
|
1598
|
+
} catch (cause) {
|
|
1599
|
+
if (!this.isCurrentLoader(loader, runId) || isAbortLikeError(cause)) return;
|
|
1600
|
+
try {
|
|
1601
|
+
await this.closeCurrentLoader(loader, runId);
|
|
1602
|
+
} catch {}
|
|
1603
|
+
if (!this.isLifecycleContextCurrent(context)) return;
|
|
1604
|
+
if (isRecoverableLoaderError(cause)) {
|
|
1605
|
+
this.dependencies.onRecoverableFailure(cause, context);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const code = cause instanceof HttpFlvLoaderError ? cause.code : "RIVMUX_HTTP_LOADER_FAILED";
|
|
1609
|
+
this.dependencies.onFailure("network", code, "HTTP Fetch loader failed.", cause);
|
|
1610
|
+
} finally {
|
|
1611
|
+
if (this.isCurrentLoader(loader, runId)) try {
|
|
1612
|
+
await this.closeCurrentLoader(loader, runId);
|
|
1613
|
+
} catch (cause) {
|
|
1614
|
+
if (this.isLifecycleContextCurrent(context)) this.dependencies.onFailure("network", "RIVMUX_HTTP_LOADER_CLOSE_FAILED", "HTTP Fetch loader failed to close.", cause);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async processEvents(events, context) {
|
|
1619
|
+
for (const event of events) {
|
|
1620
|
+
if (!this.isLifecycleContextCurrent(context)) return false;
|
|
1621
|
+
switch (event.type) {
|
|
1622
|
+
case "mediaInfo":
|
|
1623
|
+
this.dependencies.onMessage({
|
|
1624
|
+
type: "media-info",
|
|
1625
|
+
mediaInfo: coreMediaInfoToPlayerMediaInfo(event.data)
|
|
1626
|
+
});
|
|
1627
|
+
break;
|
|
1628
|
+
case "warning":
|
|
1629
|
+
this.dependencies.onMessage({
|
|
1630
|
+
type: "warning",
|
|
1631
|
+
warning: coreWarningToPlayerWarning(event.data)
|
|
1632
|
+
});
|
|
1633
|
+
break;
|
|
1634
|
+
case "fatalError":
|
|
1635
|
+
this.dependencies.onPlayerError(coreErrorToPlayerError(event.data));
|
|
1636
|
+
return false;
|
|
1637
|
+
case "initSegment":
|
|
1638
|
+
if (!await this.appendController.flush(context)) return false;
|
|
1639
|
+
if (!await this.appendToMse(context, () => this.mse?.appendInitSegment(event.data))) return false;
|
|
1640
|
+
if (!this.isLifecycleContextCurrent(context)) return false;
|
|
1641
|
+
this.outputBytes += event.data.bytes.byteLength;
|
|
1642
|
+
await this.dependencies.applyLatencyPolicy(context);
|
|
1643
|
+
break;
|
|
1644
|
+
case "mediaSegment": {
|
|
1645
|
+
const append = this.appendController.push(event.data, context);
|
|
1646
|
+
if (append !== void 0 && !await append) return false;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return this.appendController.waitForTail();
|
|
1652
|
+
}
|
|
1653
|
+
async appendToMse(context, append) {
|
|
1654
|
+
try {
|
|
1655
|
+
if ((await raceLifecycleOperation(Promise.resolve(append()), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
|
|
1656
|
+
return true;
|
|
1657
|
+
} catch (cause) {
|
|
1658
|
+
if (!this.isLifecycleContextCurrent(context)) return false;
|
|
1659
|
+
if (isQuotaExceededError(cause) && await this.retryAppendAfterQuotaCleanup(context, append)) return true;
|
|
1660
|
+
if (cause instanceof MseUnsupportedMimeError) {
|
|
1661
|
+
this.dependencies.onFailure("unsupported", "RIVMUX_UNSUPPORTED_MSE_CODEC", cause.message, cause);
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
this.dependencies.onFailure("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", cause);
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async retryAppendAfterQuotaCleanup(context, append) {
|
|
1669
|
+
const cutoff = this.dependencies.quotaCleanupCutoff();
|
|
1670
|
+
if (cutoff === void 0 || cutoff <= 0) return false;
|
|
1671
|
+
try {
|
|
1672
|
+
if ((await raceLifecycleOperation(Promise.resolve(this.cleanupBefore(cutoff, true)), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
|
|
1673
|
+
if ((await raceLifecycleOperation(Promise.resolve(append()), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
|
|
1674
|
+
this.dependencies.onMessage({
|
|
1675
|
+
type: "warning",
|
|
1676
|
+
warning: {
|
|
1677
|
+
code: "RIVMUX_MSE_QUOTA_RETRY",
|
|
1678
|
+
message: "MSE quota was exceeded; old buffered ranges were cleaned before retrying append."
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
return true;
|
|
1682
|
+
} catch {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
function isQuotaExceededError(cause) {
|
|
1688
|
+
return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "QuotaExceededError";
|
|
1689
|
+
}
|
|
1690
|
+
//#endregion
|
|
1691
|
+
//#region src/runtime/index.ts
|
|
1692
|
+
var RuntimeWorker = class {
|
|
1693
|
+
port;
|
|
1094
1694
|
detectRuntime;
|
|
1095
1695
|
now;
|
|
1696
|
+
sleep;
|
|
1697
|
+
random;
|
|
1096
1698
|
state = "idle";
|
|
1097
1699
|
url;
|
|
1098
1700
|
options;
|
|
1099
|
-
|
|
1100
|
-
loader;
|
|
1101
|
-
transmuxCore;
|
|
1701
|
+
session;
|
|
1102
1702
|
latencyController;
|
|
1103
1703
|
videoState;
|
|
1104
1704
|
lastLatencyMetrics = {};
|
|
1105
1705
|
statsTimer;
|
|
1106
1706
|
statsTickInFlight = false;
|
|
1107
|
-
loaderRunId = 0;
|
|
1108
|
-
outputBytes = 0;
|
|
1109
1707
|
appendQueueMaxLength = 0;
|
|
1110
1708
|
appendQueueMaxBytes = 0;
|
|
1709
|
+
commandTail = Promise.resolve();
|
|
1710
|
+
lifecycleGeneration = 0;
|
|
1711
|
+
lifecycleAbortController = new AbortController();
|
|
1712
|
+
fatalCleanupPromise = Promise.resolve();
|
|
1713
|
+
recoveryPromise;
|
|
1714
|
+
connectionAttempt = 1;
|
|
1715
|
+
recoveryStartedAt;
|
|
1716
|
+
pendingRecovery;
|
|
1111
1717
|
constructor(port, dependencies = {}) {
|
|
1112
1718
|
this.port = port;
|
|
1113
|
-
this.createMseController = dependencies.createMseController ?? (() => new MseController());
|
|
1114
|
-
this.createLoader = dependencies.createLoader ?? ((config) => new HttpFlvLoader(config));
|
|
1115
|
-
this.createTransmuxCore = dependencies.createTransmuxCore ?? ((options) => loadWasmTransmuxCoreHost(options.runtime.wasmUrl));
|
|
1116
1719
|
this.detectRuntime = dependencies.detectRuntime ?? detectWorkerRuntime;
|
|
1117
1720
|
this.now = dependencies.now ?? (() => performance.now());
|
|
1721
|
+
this.sleep = dependencies.sleep ?? waitForDelay;
|
|
1722
|
+
this.random = dependencies.random ?? Math.random;
|
|
1723
|
+
this.session = new RuntimeSession({
|
|
1724
|
+
...dependencies,
|
|
1725
|
+
isStarted: () => this.state === "started",
|
|
1726
|
+
isLifecycleContextCurrent: (context) => this.isLifecycleContextCurrent(context),
|
|
1727
|
+
onMediaAppended: async (context) => {
|
|
1728
|
+
this.markRecovered(context);
|
|
1729
|
+
await this.applyLatencyPolicy(context);
|
|
1730
|
+
},
|
|
1731
|
+
onAppendError: (cause, context) => {
|
|
1732
|
+
if (this.isLifecycleContextCurrent(context)) this.fail("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", true, cause);
|
|
1733
|
+
},
|
|
1734
|
+
onLoaderClosing: () => this.stopStatsTimer(),
|
|
1735
|
+
onStats: (stats) => this.postStats(stats),
|
|
1736
|
+
onMessage: (message) => this.post(message),
|
|
1737
|
+
onRecoverableFailure: (cause, context) => this.scheduleRecovery(cause, context),
|
|
1738
|
+
onFailure: (kind, code, message, cause) => this.fail(kind, code, message, true, cause),
|
|
1739
|
+
onPlayerError: (error) => this.failWithError(error),
|
|
1740
|
+
applyLatencyPolicy: (context) => this.applyLatencyPolicy(context),
|
|
1741
|
+
quotaCleanupCutoff: () => this.quotaCleanupCutoff()
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
handleCommand(command) {
|
|
1745
|
+
if (command.type === "stop" || command.type === "destroy") this.invalidateLifecycle();
|
|
1746
|
+
const context = {
|
|
1747
|
+
generation: this.lifecycleGeneration,
|
|
1748
|
+
signal: this.lifecycleAbortController.signal
|
|
1749
|
+
};
|
|
1750
|
+
const handling = this.commandTail.then(() => this.executeCommand(command, context));
|
|
1751
|
+
this.commandTail = handling.catch(() => void 0);
|
|
1752
|
+
return handling;
|
|
1118
1753
|
}
|
|
1119
|
-
async
|
|
1754
|
+
async executeCommand(command, context) {
|
|
1120
1755
|
if (this.state === "destroyed") return;
|
|
1121
|
-
if (
|
|
1756
|
+
if ((command.type === "attach-media-source" || command.type === "start") && !this.isLifecycleContextCurrent(context)) return;
|
|
1757
|
+
if (this.state === "fatal-error") {
|
|
1758
|
+
if (command.type === "stop") this.post({ type: "stopped" });
|
|
1759
|
+
if (command.type !== "destroy") return;
|
|
1760
|
+
}
|
|
1122
1761
|
try {
|
|
1123
1762
|
switch (command.type) {
|
|
1124
1763
|
case "init":
|
|
@@ -1136,22 +1775,17 @@ var RuntimeWorker = class {
|
|
|
1136
1775
|
this.post({ type: "ready" });
|
|
1137
1776
|
return;
|
|
1138
1777
|
case "attach-media-source":
|
|
1139
|
-
await this.attachMediaSource();
|
|
1778
|
+
await this.attachMediaSource(context);
|
|
1140
1779
|
return;
|
|
1141
1780
|
case "start":
|
|
1142
|
-
await this.start();
|
|
1781
|
+
await this.start(context);
|
|
1143
1782
|
return;
|
|
1144
1783
|
case "stop":
|
|
1145
1784
|
await this.stop();
|
|
1146
1785
|
return;
|
|
1147
|
-
case "update-options":
|
|
1148
|
-
this.options = this.options === void 0 ? void 0 : mergeOptions(this.options, command.options);
|
|
1149
|
-
if (this.options !== void 0) this.latencyController = createLatencyController(this.options);
|
|
1150
|
-
return;
|
|
1151
1786
|
case "video-state":
|
|
1152
1787
|
this.videoState = command.state;
|
|
1153
|
-
await this.applyLatencyPolicy();
|
|
1154
|
-
this.postStats();
|
|
1788
|
+
if (!(await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled && this.isLifecycleContextCurrent(context) && this.state === "started") this.postStats();
|
|
1155
1789
|
return;
|
|
1156
1790
|
case "playback-control-result":
|
|
1157
1791
|
this.latencyController?.recordPlaybackControlResult(command.result);
|
|
@@ -1164,267 +1798,245 @@ var RuntimeWorker = class {
|
|
|
1164
1798
|
this.fail("runtime", "RIVMUX_WORKER_COMMAND_FAILED", "Worker command failed.", true, cause);
|
|
1165
1799
|
}
|
|
1166
1800
|
}
|
|
1167
|
-
async attachMediaSource() {
|
|
1801
|
+
async attachMediaSource(context) {
|
|
1168
1802
|
if (this.state === "idle") {
|
|
1169
1803
|
this.fail("runtime", "RIVMUX_WORKER_NOT_INITIALIZED", "Worker must be initialized before attach.", true);
|
|
1170
1804
|
return;
|
|
1171
1805
|
}
|
|
1172
|
-
if (this.mse === void 0) this.mse = this.createMseController();
|
|
1173
1806
|
try {
|
|
1174
|
-
const handle = await this.
|
|
1807
|
+
const handle = await this.session.attach(context);
|
|
1808
|
+
if (handle === void 0 || context.generation !== this.lifecycleGeneration) return;
|
|
1175
1809
|
this.post({
|
|
1176
1810
|
type: "media-source-handle",
|
|
1177
1811
|
handle
|
|
1178
1812
|
}, [handle]);
|
|
1179
1813
|
this.state = "attached";
|
|
1180
1814
|
} catch (cause) {
|
|
1815
|
+
if (context.generation !== this.lifecycleGeneration) return;
|
|
1181
1816
|
this.fail("mse", "RIVMUX_MSE_ATTACH_FAILED", "MSE media source attachment failed.", true, cause);
|
|
1182
1817
|
}
|
|
1183
1818
|
}
|
|
1184
|
-
async start() {
|
|
1819
|
+
async start(context) {
|
|
1185
1820
|
const options = this.options;
|
|
1186
|
-
if (this.
|
|
1821
|
+
if (!this.session.hasMse || options === void 0 || this.state === "idle" || this.state === "ready") {
|
|
1187
1822
|
this.fail("runtime", "RIVMUX_WORKER_START_REQUIRES_ATTACH", "Worker start requires an attached MediaSource.", true);
|
|
1188
1823
|
return;
|
|
1189
1824
|
}
|
|
1190
|
-
if (this.state === "started")
|
|
1191
|
-
|
|
1825
|
+
if (this.state === "started") {
|
|
1826
|
+
this.post({ type: "started" });
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1192
1829
|
try {
|
|
1193
|
-
const
|
|
1194
|
-
if (
|
|
1830
|
+
const transmuxCore = await this.session.createCore(options, context);
|
|
1831
|
+
if (context.generation !== this.lifecycleGeneration) return;
|
|
1832
|
+
if (transmuxCore === void 0) {
|
|
1195
1833
|
this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available.", true);
|
|
1196
1834
|
return;
|
|
1197
1835
|
}
|
|
1198
|
-
transmuxCore
|
|
1836
|
+
this.session.start(transmuxCore, context);
|
|
1199
1837
|
} catch (cause) {
|
|
1838
|
+
if (context.generation !== this.lifecycleGeneration) return;
|
|
1200
1839
|
this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available.", true, cause);
|
|
1201
1840
|
return;
|
|
1202
1841
|
}
|
|
1203
|
-
this.transmuxCore?.destroy();
|
|
1204
|
-
this.transmuxCore = transmuxCore;
|
|
1205
1842
|
this.state = "started";
|
|
1206
|
-
this.outputBytes = 0;
|
|
1207
1843
|
this.appendQueueMaxLength = 0;
|
|
1208
1844
|
this.appendQueueMaxBytes = 0;
|
|
1845
|
+
if ((await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return;
|
|
1209
1846
|
this.startStatsTimer();
|
|
1210
|
-
await this.applyLatencyPolicy();
|
|
1211
1847
|
this.postStats();
|
|
1212
|
-
this.startLoader();
|
|
1848
|
+
if (!this.startLoader(context)) return;
|
|
1849
|
+
if (!this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1850
|
+
this.post({ type: "started" });
|
|
1213
1851
|
}
|
|
1214
1852
|
async stop() {
|
|
1853
|
+
await this.waitForRecovery();
|
|
1215
1854
|
await this.closeLoader();
|
|
1216
|
-
this.
|
|
1217
|
-
this.mse = void 0;
|
|
1855
|
+
this.session.destroyMse();
|
|
1218
1856
|
this.latencyController?.reset();
|
|
1219
1857
|
this.videoState = void 0;
|
|
1220
1858
|
this.lastLatencyMetrics = {};
|
|
1859
|
+
this.resetRecoveryCycle();
|
|
1221
1860
|
this.state = "stopped";
|
|
1222
1861
|
this.post({ type: "stopped" });
|
|
1223
1862
|
}
|
|
1224
1863
|
async destroy() {
|
|
1864
|
+
await this.fatalCleanupPromise;
|
|
1865
|
+
await this.waitForRecovery();
|
|
1225
1866
|
await this.closeLoader();
|
|
1226
|
-
this.
|
|
1227
|
-
this.mse = void 0;
|
|
1867
|
+
this.session.destroyMse();
|
|
1228
1868
|
this.latencyController?.reset();
|
|
1229
1869
|
this.videoState = void 0;
|
|
1230
1870
|
this.lastLatencyMetrics = {};
|
|
1871
|
+
this.resetRecoveryCycle();
|
|
1231
1872
|
this.state = "destroyed";
|
|
1232
1873
|
this.post({ type: "destroyed" });
|
|
1233
1874
|
this.port.close();
|
|
1234
1875
|
}
|
|
1235
|
-
startLoader() {
|
|
1876
|
+
startLoader(context) {
|
|
1236
1877
|
const options = this.options;
|
|
1237
1878
|
const url = this.url;
|
|
1238
1879
|
if (options === void 0 || url === void 0) {
|
|
1239
1880
|
this.fail("runtime", "RIVMUX_WORKER_NOT_INITIALIZED", "Worker must be initialized before loader start.", true);
|
|
1240
|
-
return;
|
|
1881
|
+
return false;
|
|
1241
1882
|
}
|
|
1242
|
-
|
|
1883
|
+
this.session.runLoader({
|
|
1243
1884
|
url,
|
|
1244
1885
|
network: options.network
|
|
1886
|
+
}, context);
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
async closeLoader() {
|
|
1890
|
+
this.stopStatsTimer();
|
|
1891
|
+
await this.session.close();
|
|
1892
|
+
}
|
|
1893
|
+
scheduleRecovery(cause, context) {
|
|
1894
|
+
if (!this.isLifecycleContextCurrent(context) || this.state !== "started" || this.recoveryPromise !== void 0) return;
|
|
1895
|
+
const recovery = this.recover(cause, context).finally(() => {
|
|
1896
|
+
if (this.recoveryPromise === recovery) this.recoveryPromise = void 0;
|
|
1245
1897
|
});
|
|
1246
|
-
|
|
1247
|
-
this.loaderRunId = runId;
|
|
1248
|
-
this.loader = loader;
|
|
1249
|
-
this.runLoader(loader, runId);
|
|
1898
|
+
this.recoveryPromise = recovery;
|
|
1250
1899
|
}
|
|
1251
|
-
async
|
|
1900
|
+
async recover(cause, context) {
|
|
1901
|
+
const options = this.options;
|
|
1902
|
+
if (options === void 0 || !this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1903
|
+
this.recoveryStartedAt ??= this.now();
|
|
1904
|
+
if (this.connectionAttempt >= options.network.retry.maxAttempts) {
|
|
1905
|
+
this.fail("network", "RIVMUX_RECONNECT_EXHAUSTED", "Live stream reconnect attempts were exhausted.", true, cause);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const failedAttempt = this.connectionAttempt;
|
|
1909
|
+
const attempt = failedAttempt + 1;
|
|
1910
|
+
this.connectionAttempt = attempt;
|
|
1911
|
+
const delayMs = getRetryDelayMs(options.network.retry, failedAttempt, this.random);
|
|
1912
|
+
this.post({
|
|
1913
|
+
type: "reconnecting",
|
|
1914
|
+
info: {
|
|
1915
|
+
attempt,
|
|
1916
|
+
maxAttempts: options.network.retry.maxAttempts,
|
|
1917
|
+
delayMs,
|
|
1918
|
+
reason: cause.reason ?? "network-error"
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
await this.closeLoader();
|
|
1922
|
+
this.session.destroyMse();
|
|
1923
|
+
this.latencyController?.reset();
|
|
1924
|
+
this.lastLatencyMetrics = {};
|
|
1925
|
+
if (!this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1252
1926
|
try {
|
|
1253
|
-
await
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1927
|
+
if ((await raceLifecycleOperation(this.sleep(delayMs, context.signal), context.signal)).cancelled || !this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1928
|
+
} catch (delayError) {
|
|
1929
|
+
if (this.isLifecycleContextCurrent(context)) this.fail("network", "RIVMUX_RECONNECT_DELAY_FAILED", "Live stream reconnect delay failed.", true, delayError);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
let handle;
|
|
1933
|
+
try {
|
|
1934
|
+
handle = await this.session.attach(context);
|
|
1935
|
+
} catch (attachError) {
|
|
1936
|
+
if (this.isLifecycleContextCurrent(context)) this.fail("mse", "RIVMUX_MSE_ATTACH_FAILED", "MSE media source attachment failed during reconnect.", true, attachError);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (handle === void 0 || !this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1940
|
+
this.post({
|
|
1941
|
+
type: "media-source-handle",
|
|
1942
|
+
handle
|
|
1943
|
+
}, [handle]);
|
|
1944
|
+
try {
|
|
1945
|
+
const core = await this.session.createCore(options, context);
|
|
1946
|
+
if (!this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1947
|
+
if (core === void 0) {
|
|
1948
|
+
this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available during reconnect.", true);
|
|
1949
|
+
return;
|
|
1265
1950
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
this.fail("network", getNetworkErrorCode(cause), "HTTP Fetch loader failed.", true, cause);
|
|
1951
|
+
this.session.start(core, context);
|
|
1952
|
+
} catch (coreError) {
|
|
1953
|
+
if (this.isLifecycleContextCurrent(context)) this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available during reconnect.", true, coreError);
|
|
1270
1954
|
return;
|
|
1271
|
-
} finally {
|
|
1272
|
-
if (this.isCurrentLoader(loader, runId)) await this.closeCurrentLoader(loader, runId);
|
|
1273
1955
|
}
|
|
1956
|
+
this.pendingRecovery = {
|
|
1957
|
+
attempt,
|
|
1958
|
+
downtimeMs: 0
|
|
1959
|
+
};
|
|
1960
|
+
this.appendQueueMaxLength = 0;
|
|
1961
|
+
this.appendQueueMaxBytes = 0;
|
|
1962
|
+
this.startStatsTimer();
|
|
1963
|
+
this.postStats();
|
|
1964
|
+
this.recoveryPromise = void 0;
|
|
1965
|
+
this.startLoader(context);
|
|
1966
|
+
}
|
|
1967
|
+
markRecovered(context) {
|
|
1968
|
+
const recovery = this.pendingRecovery;
|
|
1969
|
+
const startedAt = this.recoveryStartedAt;
|
|
1970
|
+
if (recovery === void 0 || startedAt === void 0 || !this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1971
|
+
this.pendingRecovery = void 0;
|
|
1972
|
+
this.recoveryStartedAt = void 0;
|
|
1973
|
+
this.connectionAttempt = 1;
|
|
1974
|
+
this.post({
|
|
1975
|
+
type: "recovered",
|
|
1976
|
+
info: {
|
|
1977
|
+
attempt: recovery.attempt,
|
|
1978
|
+
downtimeMs: Math.max(0, this.now() - startedAt)
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1274
1981
|
}
|
|
1275
|
-
async
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
this.loader = void 0;
|
|
1280
|
-
this.loaderRunId += 1;
|
|
1281
|
-
this.transmuxCore?.destroy();
|
|
1282
|
-
this.transmuxCore = void 0;
|
|
1283
|
-
await loader.close();
|
|
1284
|
-
}
|
|
1285
|
-
async closeCurrentLoader(loader, runId) {
|
|
1286
|
-
if (this.loader !== loader || this.loaderRunId !== runId) return;
|
|
1287
|
-
this.stopStatsTimer();
|
|
1288
|
-
this.loader = void 0;
|
|
1289
|
-
this.loaderRunId += 1;
|
|
1290
|
-
this.transmuxCore?.destroy();
|
|
1291
|
-
this.transmuxCore = void 0;
|
|
1292
|
-
await loader.close();
|
|
1982
|
+
async waitForRecovery() {
|
|
1983
|
+
try {
|
|
1984
|
+
await this.recoveryPromise;
|
|
1985
|
+
} catch {}
|
|
1293
1986
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1987
|
+
resetRecoveryCycle() {
|
|
1988
|
+
this.connectionAttempt = 1;
|
|
1989
|
+
this.recoveryStartedAt = void 0;
|
|
1990
|
+
this.pendingRecovery = void 0;
|
|
1296
1991
|
}
|
|
1297
1992
|
postStats(loaderStats) {
|
|
1298
|
-
const metrics = this.lastLatencyMetrics;
|
|
1299
1993
|
const mseStats = this.collectMseStats();
|
|
1300
|
-
const loaderSnapshot = loaderStats ?? this.
|
|
1994
|
+
const loaderSnapshot = loaderStats ?? this.session.loaderStats;
|
|
1301
1995
|
this.post({
|
|
1302
1996
|
type: "stats",
|
|
1303
|
-
stats: {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
outputBytes: this.
|
|
1308
|
-
appendQueueLength: mseStats.appendQueueLength,
|
|
1309
|
-
appendQueueBytes: mseStats.appendQueueBytes,
|
|
1997
|
+
stats: createPlayerStats({
|
|
1998
|
+
loaderStats: loaderSnapshot,
|
|
1999
|
+
mseStats,
|
|
2000
|
+
latencyMetrics: this.lastLatencyMetrics,
|
|
2001
|
+
outputBytes: this.session.emittedBytes,
|
|
1310
2002
|
appendQueueMaxLength: this.appendQueueMaxLength,
|
|
1311
2003
|
appendQueueMaxBytes: this.appendQueueMaxBytes,
|
|
1312
|
-
loaderPaused: this.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
bufferedStart: metrics.bufferedStart ?? this.mse?.bufferedStart,
|
|
1316
|
-
bufferedEnd: metrics.bufferedEnd ?? this.mse?.bufferedEnd,
|
|
1317
|
-
bufferedDuration: metrics.bufferedDuration ?? this.mse?.bufferedDuration,
|
|
1318
|
-
bufferedRangeCount: mseStats.bufferedRangeCount,
|
|
1319
|
-
currentTime: metrics.currentTime,
|
|
1320
|
-
liveLatency: metrics.liveLatency,
|
|
1321
|
-
playbackRate: metrics.playbackRate,
|
|
1322
|
-
readyState: metrics.readyState,
|
|
1323
|
-
droppedFrames: metrics.droppedFrames
|
|
1324
|
-
}
|
|
2004
|
+
loaderPaused: this.session.loaderPaused,
|
|
2005
|
+
nowMs: this.now()
|
|
2006
|
+
})
|
|
1325
2007
|
});
|
|
1326
2008
|
}
|
|
1327
|
-
async processTransmuxEvents(events) {
|
|
1328
|
-
for (const event of events) switch (event.type) {
|
|
1329
|
-
case "mediaInfo":
|
|
1330
|
-
this.post({
|
|
1331
|
-
type: "media-info",
|
|
1332
|
-
mediaInfo: coreMediaInfoToPlayerMediaInfo(event.data)
|
|
1333
|
-
});
|
|
1334
|
-
break;
|
|
1335
|
-
case "warning":
|
|
1336
|
-
this.post({
|
|
1337
|
-
type: "warning",
|
|
1338
|
-
warning: coreWarningToPlayerWarning(event.data)
|
|
1339
|
-
});
|
|
1340
|
-
break;
|
|
1341
|
-
case "fatalError":
|
|
1342
|
-
this.failWithError(coreErrorToPlayerError(event.data));
|
|
1343
|
-
return false;
|
|
1344
|
-
case "initSegment":
|
|
1345
|
-
if (!await this.appendToMse(() => this.mse?.appendInitSegment(event.data))) return false;
|
|
1346
|
-
this.outputBytes += event.data.bytes.byteLength;
|
|
1347
|
-
await this.applyLatencyPolicy();
|
|
1348
|
-
break;
|
|
1349
|
-
case "mediaSegment":
|
|
1350
|
-
if (!await this.appendToMse(() => this.mse?.appendMediaSegment(event.data))) return false;
|
|
1351
|
-
this.outputBytes += event.data.bytes.byteLength;
|
|
1352
|
-
await this.applyLatencyPolicy();
|
|
1353
|
-
break;
|
|
1354
|
-
case "probeResult":
|
|
1355
|
-
case "videoConfig":
|
|
1356
|
-
case "audioConfig":
|
|
1357
|
-
case "videoSample":
|
|
1358
|
-
case "audioSample":
|
|
1359
|
-
case "metadata":
|
|
1360
|
-
case "discontinuity": break;
|
|
1361
|
-
}
|
|
1362
|
-
return true;
|
|
1363
|
-
}
|
|
1364
2009
|
collectMseStats() {
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
sourceBufferCount: this.mse?.sourceBufferCount ?? 0,
|
|
1374
|
-
bufferedRangeCount: this.mse?.bufferedRangeCount ?? 0
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
async appendToMse(append) {
|
|
1378
|
-
try {
|
|
1379
|
-
await append();
|
|
1380
|
-
return true;
|
|
1381
|
-
} catch (cause) {
|
|
1382
|
-
if (isQuotaExceededError(cause) && await this.retryAppendAfterQuotaCleanup(append)) return true;
|
|
1383
|
-
this.fail("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", true, cause);
|
|
1384
|
-
return false;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
async retryAppendAfterQuotaCleanup(append) {
|
|
1388
|
-
const mse = this.mse;
|
|
1389
|
-
const cutoff = this.quotaCleanupCutoff();
|
|
1390
|
-
if (mse === void 0 || cutoff === void 0 || cutoff <= 0) return false;
|
|
1391
|
-
try {
|
|
1392
|
-
await mse.cleanupBefore(cutoff, { force: true });
|
|
1393
|
-
await append();
|
|
1394
|
-
this.post({
|
|
1395
|
-
type: "warning",
|
|
1396
|
-
warning: {
|
|
1397
|
-
code: "RIVMUX_MSE_QUOTA_RETRY",
|
|
1398
|
-
message: "MSE quota was exceeded; old buffered ranges were cleaned before retrying append."
|
|
1399
|
-
}
|
|
1400
|
-
});
|
|
1401
|
-
return true;
|
|
1402
|
-
} catch {
|
|
1403
|
-
return false;
|
|
1404
|
-
}
|
|
2010
|
+
const stats = this.session.collectMseStats();
|
|
2011
|
+
const highWaterMark = updateAppendQueueHighWaterMark({
|
|
2012
|
+
length: this.appendQueueMaxLength,
|
|
2013
|
+
bytes: this.appendQueueMaxBytes
|
|
2014
|
+
}, stats);
|
|
2015
|
+
this.appendQueueMaxLength = highWaterMark.length;
|
|
2016
|
+
this.appendQueueMaxBytes = highWaterMark.bytes;
|
|
2017
|
+
return { ...stats };
|
|
1405
2018
|
}
|
|
1406
2019
|
quotaCleanupCutoff() {
|
|
1407
2020
|
const backwardBuffer = this.options?.latency.backwardBuffer ?? 0;
|
|
1408
2021
|
const currentTime = this.videoState?.currentTime;
|
|
1409
2022
|
if (currentTime !== void 0 && Number.isFinite(currentTime)) return Math.max(0, currentTime - backwardBuffer);
|
|
1410
|
-
const bufferedEnd = this.
|
|
2023
|
+
const bufferedEnd = this.session.collectMseStats().bufferedEnd;
|
|
1411
2024
|
return bufferedEnd === void 0 ? void 0 : Math.max(0, bufferedEnd - backwardBuffer);
|
|
1412
2025
|
}
|
|
1413
|
-
async applyLatencyPolicy() {
|
|
2026
|
+
async applyLatencyPolicy(context) {
|
|
1414
2027
|
const latencyController = this.latencyController;
|
|
1415
|
-
|
|
1416
|
-
if (latencyController === void 0 || mse === void 0) return;
|
|
1417
|
-
const loader = this.loader;
|
|
2028
|
+
if (latencyController === void 0 || !this.session.hasMse) return;
|
|
1418
2029
|
const evaluation = latencyController.evaluate({
|
|
1419
|
-
ranges:
|
|
2030
|
+
ranges: this.session.bufferedRanges,
|
|
1420
2031
|
videoState: this.videoState,
|
|
1421
|
-
loaderPaused:
|
|
2032
|
+
loaderPaused: this.session.loaderPaused,
|
|
1422
2033
|
nowMs: this.now()
|
|
1423
2034
|
});
|
|
1424
2035
|
this.lastLatencyMetrics = evaluation.metrics;
|
|
1425
|
-
if (evaluation.cleanupBefore !== void 0) await
|
|
1426
|
-
if (
|
|
1427
|
-
|
|
2036
|
+
if (evaluation.cleanupBefore !== void 0) await this.session.cleanupBefore(evaluation.cleanupBefore);
|
|
2037
|
+
if (context !== void 0 && !this.isLifecycleContextCurrent(context)) return;
|
|
2038
|
+
if (evaluation.loaderCommand === "pause") this.session.pauseLoader();
|
|
2039
|
+
else if (evaluation.loaderCommand === "resume") this.session.resumeLoader();
|
|
1428
2040
|
if (evaluation.playbackControl !== void 0) this.post({
|
|
1429
2041
|
type: "playback-control",
|
|
1430
2042
|
action: evaluation.playbackControl
|
|
@@ -1447,15 +2059,25 @@ var RuntimeWorker = class {
|
|
|
1447
2059
|
async emitStatsTick() {
|
|
1448
2060
|
if (this.statsTickInFlight || this.state !== "started") return;
|
|
1449
2061
|
this.statsTickInFlight = true;
|
|
2062
|
+
const context = this.currentLifecycleContext();
|
|
1450
2063
|
try {
|
|
1451
|
-
await this.applyLatencyPolicy();
|
|
1452
|
-
this.postStats();
|
|
2064
|
+
if (!(await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled && this.isLifecycleContextCurrent(context) && this.state === "started") this.postStats();
|
|
1453
2065
|
} catch (cause) {
|
|
2066
|
+
if (!this.isLifecycleContextCurrent(context)) return;
|
|
1454
2067
|
this.fail("mse", "RIVMUX_MSE_LATENCY_POLICY_FAILED", "MSE latency policy failed.", true, cause);
|
|
1455
2068
|
} finally {
|
|
1456
2069
|
this.statsTickInFlight = false;
|
|
1457
2070
|
}
|
|
1458
2071
|
}
|
|
2072
|
+
currentLifecycleContext() {
|
|
2073
|
+
return {
|
|
2074
|
+
generation: this.lifecycleGeneration,
|
|
2075
|
+
signal: this.lifecycleAbortController.signal
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
isLifecycleContextCurrent(context) {
|
|
2079
|
+
return !context.signal.aborted && context.generation === this.lifecycleGeneration;
|
|
2080
|
+
}
|
|
1459
2081
|
fail(kind, code, message, terminal, cause) {
|
|
1460
2082
|
const error = cause === void 0 ? {
|
|
1461
2083
|
kind,
|
|
@@ -1472,6 +2094,7 @@ var RuntimeWorker = class {
|
|
|
1472
2094
|
this.failWithError(error);
|
|
1473
2095
|
}
|
|
1474
2096
|
failWithError(error) {
|
|
2097
|
+
if (this.state === "destroyed") return;
|
|
1475
2098
|
if (error.terminal) this.enterFatalErrorState();
|
|
1476
2099
|
this.post({
|
|
1477
2100
|
type: "error",
|
|
@@ -1479,39 +2102,61 @@ var RuntimeWorker = class {
|
|
|
1479
2102
|
});
|
|
1480
2103
|
}
|
|
1481
2104
|
enterFatalErrorState() {
|
|
1482
|
-
if (this.state === "fatal-error") return;
|
|
2105
|
+
if (this.state === "fatal-error" || this.state === "destroyed") return;
|
|
1483
2106
|
this.state = "fatal-error";
|
|
1484
|
-
this.
|
|
1485
|
-
this.
|
|
1486
|
-
this.
|
|
2107
|
+
this.invalidateLifecycle();
|
|
2108
|
+
this.session.discardAppend();
|
|
2109
|
+
this.fatalCleanupPromise = this.closeLoader().catch(() => void 0);
|
|
2110
|
+
this.session.destroyMse();
|
|
1487
2111
|
this.latencyController?.reset();
|
|
1488
2112
|
this.videoState = void 0;
|
|
1489
2113
|
this.lastLatencyMetrics = {};
|
|
2114
|
+
this.resetRecoveryCycle();
|
|
1490
2115
|
}
|
|
1491
2116
|
post(message, transfer) {
|
|
1492
2117
|
this.port.postMessage(message, transfer);
|
|
1493
2118
|
}
|
|
2119
|
+
invalidateLifecycle() {
|
|
2120
|
+
this.lifecycleGeneration += 1;
|
|
2121
|
+
this.lifecycleAbortController.abort();
|
|
2122
|
+
this.lifecycleAbortController = new AbortController();
|
|
2123
|
+
}
|
|
1494
2124
|
};
|
|
1495
|
-
function
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
2125
|
+
function waitForDelay(delayMs, signal) {
|
|
2126
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
2127
|
+
if (signal.aborted) return Promise.reject(new DOMException("Reconnect delay was aborted.", "AbortError"));
|
|
2128
|
+
return new Promise((resolve, reject) => {
|
|
2129
|
+
const cleanup = () => {
|
|
2130
|
+
clearTimeout(timer);
|
|
2131
|
+
signal.removeEventListener("abort", onAbort);
|
|
2132
|
+
};
|
|
2133
|
+
const onAbort = () => {
|
|
2134
|
+
cleanup();
|
|
2135
|
+
reject(new DOMException("Reconnect delay was aborted.", "AbortError"));
|
|
2136
|
+
};
|
|
2137
|
+
const timer = setTimeout(() => {
|
|
2138
|
+
cleanup();
|
|
2139
|
+
resolve();
|
|
2140
|
+
}, delayMs);
|
|
2141
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2142
|
+
});
|
|
1502
2143
|
}
|
|
1503
2144
|
function serializeCause(cause) {
|
|
1504
2145
|
if (cause instanceof Error) return {
|
|
1505
2146
|
name: cause.name,
|
|
1506
2147
|
message: cause.message
|
|
1507
2148
|
};
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
2149
|
+
if (typeof cause === "object" && cause !== null) {
|
|
2150
|
+
const value = cause;
|
|
2151
|
+
return {
|
|
2152
|
+
name: typeof value.name === "string" ? value.name : "Error",
|
|
2153
|
+
message: typeof value.message === "string" ? value.message : String(cause)
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
return {
|
|
2157
|
+
name: "Error",
|
|
2158
|
+
message: String(cause)
|
|
2159
|
+
};
|
|
1515
2160
|
}
|
|
1516
2161
|
function detectWorkerRuntime() {
|
|
1517
2162
|
if (typeof fetch !== "function") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_FETCH", "Fetch is not available in this worker runtime.");
|
|
@@ -1520,7 +2165,6 @@ function detectWorkerRuntime() {
|
|
|
1520
2165
|
if (typeof MediaSource === "undefined") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_MSE", "MediaSource is not available in this worker runtime.");
|
|
1521
2166
|
if (MediaSource.canConstructInDedicatedWorker !== true) return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_WORKER_MSE", "MediaSource cannot be constructed in this worker runtime.");
|
|
1522
2167
|
if (typeof MediaSource.isTypeSupported !== "function") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_MSE_TYPE_CHECK", "MediaSource.isTypeSupported is not available in this worker runtime.");
|
|
1523
|
-
for (const requirement of REQUIRED_MSE_MIME_TYPES) if (!MediaSource.isTypeSupported(requirement.mimeType)) return createUnsupportedRuntimeError(requirement.unsupportedCode, `MSE does not support ${requirement.mimeType}.`);
|
|
1524
2168
|
}
|
|
1525
2169
|
function createUnsupportedRuntimeError(code, message) {
|
|
1526
2170
|
return {
|
|
@@ -1536,38 +2180,6 @@ function createLatencyController(options) {
|
|
|
1536
2180
|
playback: options.playback
|
|
1537
2181
|
});
|
|
1538
2182
|
}
|
|
1539
|
-
function mergeOptions(current, updates) {
|
|
1540
|
-
return {
|
|
1541
|
-
playback: {
|
|
1542
|
-
...current.playback,
|
|
1543
|
-
...updates.playback
|
|
1544
|
-
},
|
|
1545
|
-
latency: {
|
|
1546
|
-
...current.latency,
|
|
1547
|
-
...updates.latency
|
|
1548
|
-
},
|
|
1549
|
-
network: {
|
|
1550
|
-
...current.network,
|
|
1551
|
-
...updates.network,
|
|
1552
|
-
headers: {
|
|
1553
|
-
...current.network.headers,
|
|
1554
|
-
...updates.network?.headers
|
|
1555
|
-
},
|
|
1556
|
-
retry: {
|
|
1557
|
-
...current.network.retry,
|
|
1558
|
-
...updates.network?.retry
|
|
1559
|
-
}
|
|
1560
|
-
},
|
|
1561
|
-
runtime: {
|
|
1562
|
-
...current.runtime,
|
|
1563
|
-
...updates.runtime
|
|
1564
|
-
},
|
|
1565
|
-
diagnostics: {
|
|
1566
|
-
...current.diagnostics,
|
|
1567
|
-
...updates.diagnostics
|
|
1568
|
-
}
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
2183
|
//#endregion
|
|
1572
2184
|
//#region src/worker-entry.ts
|
|
1573
2185
|
function startRuntimeWorker(scope) {
|