@rivmux/runtime-worker 0.5.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 +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -6
- package/dist/index.js.map +1 -1
- package/dist/rivmux-runtime-worker.d.ts +43 -17
- package/dist/rivmux-runtime-worker.d.ts.map +1 -1
- package/dist/rivmux-runtime-worker.js +472 -215
- package/dist/rivmux-runtime-worker.js.map +1 -1
- package/dist/rivmux-transmux-core.wasm +0 -0
- package/package.json +5 -9
|
@@ -141,151 +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/runtime/lifecycle.ts
|
|
145
|
-
function raceLifecycleOperation(operation, signal, onLateValue) {
|
|
146
|
-
if (signal.aborted) {
|
|
147
|
-
operation.then(onLateValue, () => void 0);
|
|
148
|
-
return Promise.resolve({ cancelled: true });
|
|
149
|
-
}
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
let settled = false;
|
|
152
|
-
const onAbort = () => {
|
|
153
|
-
if (settled) return;
|
|
154
|
-
settled = true;
|
|
155
|
-
signal.removeEventListener("abort", onAbort);
|
|
156
|
-
operation.then(onLateValue, () => void 0);
|
|
157
|
-
resolve({ cancelled: true });
|
|
158
|
-
};
|
|
159
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
160
|
-
operation.then((value) => {
|
|
161
|
-
if (settled) return;
|
|
162
|
-
settled = true;
|
|
163
|
-
signal.removeEventListener("abort", onAbort);
|
|
164
|
-
resolve({
|
|
165
|
-
cancelled: false,
|
|
166
|
-
value
|
|
167
|
-
});
|
|
168
|
-
}, (error) => {
|
|
169
|
-
if (settled) return;
|
|
170
|
-
settled = true;
|
|
171
|
-
signal.removeEventListener("abort", onAbort);
|
|
172
|
-
reject(error);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region src/runtime/options.ts
|
|
178
|
-
function mergeOptions(current, updates) {
|
|
179
|
-
return {
|
|
180
|
-
playback: {
|
|
181
|
-
...current.playback,
|
|
182
|
-
...updates.playback
|
|
183
|
-
},
|
|
184
|
-
latency: {
|
|
185
|
-
...current.latency,
|
|
186
|
-
...updates.latency
|
|
187
|
-
},
|
|
188
|
-
network: {
|
|
189
|
-
...current.network,
|
|
190
|
-
...updates.network,
|
|
191
|
-
headers: {
|
|
192
|
-
...current.network.headers,
|
|
193
|
-
...updates.network?.headers
|
|
194
|
-
},
|
|
195
|
-
retry: {
|
|
196
|
-
...current.network.retry,
|
|
197
|
-
...updates.network?.retry
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
runtime: {
|
|
201
|
-
...current.runtime,
|
|
202
|
-
...updates.runtime
|
|
203
|
-
},
|
|
204
|
-
diagnostics: {
|
|
205
|
-
...current.diagnostics,
|
|
206
|
-
...updates.diagnostics
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
//#endregion
|
|
211
|
-
//#region src/runtime/stats.ts
|
|
212
|
-
function updateAppendQueueHighWaterMark(current, mseStats) {
|
|
213
|
-
return {
|
|
214
|
-
length: Math.max(current.length, mseStats.appendQueueLength),
|
|
215
|
-
bytes: Math.max(current.bytes, mseStats.appendQueueBytes)
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function getNetworkIdleMs(stats, nowMs) {
|
|
219
|
-
const markerMs = stats?.lastChunkAtMs ?? stats?.startedAtMs;
|
|
220
|
-
if (markerMs === void 0) return;
|
|
221
|
-
return Math.max(nowMs - markerMs, 0);
|
|
222
|
-
}
|
|
223
|
-
function createPlayerStats(snapshot) {
|
|
224
|
-
const { loaderStats, mseStats, latencyMetrics } = snapshot;
|
|
225
|
-
return {
|
|
226
|
-
bytesReceived: loaderStats?.bytesReceived ?? 0,
|
|
227
|
-
currentNetworkSpeed: loaderStats?.currentNetworkSpeed ?? 0,
|
|
228
|
-
networkIdleMs: getNetworkIdleMs(loaderStats, snapshot.nowMs),
|
|
229
|
-
outputBytes: snapshot.outputBytes,
|
|
230
|
-
appendQueueLength: mseStats.appendQueueLength,
|
|
231
|
-
appendQueueBytes: mseStats.appendQueueBytes,
|
|
232
|
-
appendQueueMaxLength: snapshot.appendQueueMaxLength,
|
|
233
|
-
appendQueueMaxBytes: snapshot.appendQueueMaxBytes,
|
|
234
|
-
loaderPaused: snapshot.loaderPaused,
|
|
235
|
-
sourceBufferUpdating: mseStats.sourceBufferUpdating,
|
|
236
|
-
sourceBufferCount: mseStats.sourceBufferCount,
|
|
237
|
-
bufferedStart: latencyMetrics.bufferedStart ?? mseStats.bufferedStart,
|
|
238
|
-
bufferedEnd: latencyMetrics.bufferedEnd ?? mseStats.bufferedEnd,
|
|
239
|
-
bufferedDuration: latencyMetrics.bufferedDuration ?? mseStats.bufferedDuration,
|
|
240
|
-
bufferedRangeCount: mseStats.bufferedRangeCount,
|
|
241
|
-
currentTime: latencyMetrics.currentTime,
|
|
242
|
-
liveLatency: latencyMetrics.liveLatency,
|
|
243
|
-
playbackRate: latencyMetrics.playbackRate,
|
|
244
|
-
readyState: latencyMetrics.readyState,
|
|
245
|
-
droppedFrames: latencyMetrics.droppedFrames
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
249
|
-
//#region src/loader/retry-policy.ts
|
|
250
|
-
function createRetryPolicy(input) {
|
|
251
|
-
return {
|
|
252
|
-
maxAttempts: clampInteger(input?.maxAttempts, 1),
|
|
253
|
-
backoffMs: clampInteger(input?.backoffMs, 0)
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function getRetryDelayMs(policy, attempt) {
|
|
257
|
-
if (policy.backoffMs === 0) return 0;
|
|
258
|
-
return policy.backoffMs * Math.max(1, attempt);
|
|
259
|
-
}
|
|
260
|
-
function clampInteger(value, minimum) {
|
|
261
|
-
if (value === void 0 || !Number.isFinite(value)) return minimum;
|
|
262
|
-
return Math.max(minimum, Math.trunc(value));
|
|
263
|
-
}
|
|
264
|
-
//#endregion
|
|
265
144
|
//#region src/loader/http-flv-loader.ts
|
|
145
|
+
/** Structured failure raised by one HTTP-FLV connection. */
|
|
266
146
|
var HttpFlvLoaderError = class extends Error {
|
|
267
147
|
code;
|
|
148
|
+
phase;
|
|
149
|
+
reason;
|
|
268
150
|
status;
|
|
269
|
-
|
|
270
|
-
|
|
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 });
|
|
271
159
|
this.name = "HttpFlvLoaderError";
|
|
272
160
|
this.code = code;
|
|
273
|
-
this.
|
|
161
|
+
this.phase = normalizedOptions.phase;
|
|
162
|
+
this.reason = normalizedOptions.reason;
|
|
163
|
+
this.status = normalizedOptions.status;
|
|
164
|
+
this.cause = normalizedOptions.cause;
|
|
274
165
|
}
|
|
275
166
|
};
|
|
276
167
|
var HttpFlvLoader = class {
|
|
277
168
|
url;
|
|
278
169
|
headers;
|
|
279
170
|
credentials;
|
|
280
|
-
|
|
171
|
+
readIdleTimeoutMs;
|
|
281
172
|
fetchImpl;
|
|
282
173
|
now;
|
|
283
|
-
|
|
174
|
+
setTimer;
|
|
175
|
+
clearTimer;
|
|
284
176
|
abortController;
|
|
285
177
|
reader;
|
|
286
178
|
state = "idle";
|
|
287
179
|
pausedState = false;
|
|
288
180
|
resumeWaiters = [];
|
|
181
|
+
activeReadTimeout;
|
|
289
182
|
mutableStats = {
|
|
290
183
|
bytesReceived: 0,
|
|
291
184
|
currentNetworkSpeed: 0
|
|
@@ -294,10 +187,11 @@ var HttpFlvLoader = class {
|
|
|
294
187
|
this.url = config.url;
|
|
295
188
|
this.headers = config.network.headers;
|
|
296
189
|
this.credentials = config.network.credentials;
|
|
297
|
-
this.
|
|
190
|
+
this.readIdleTimeoutMs = config.network.readIdleTimeoutMs;
|
|
298
191
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
299
192
|
this.now = config.now ?? (() => performance.now());
|
|
300
|
-
this.
|
|
193
|
+
this.setTimer = config.setTimeout ?? ((callback, ms) => setTimeout(callback, ms));
|
|
194
|
+
this.clearTimer = config.clearTimeout ?? ((timer) => clearTimeout(timer));
|
|
301
195
|
}
|
|
302
196
|
get closed() {
|
|
303
197
|
return this.state === "closed";
|
|
@@ -309,33 +203,101 @@ var HttpFlvLoader = class {
|
|
|
309
203
|
return { ...this.mutableStats };
|
|
310
204
|
}
|
|
311
205
|
async open() {
|
|
312
|
-
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" });
|
|
313
207
|
this.state = "opening";
|
|
314
208
|
this.mutableStats.startedAtMs = this.now();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
});
|
|
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;
|
|
324
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";
|
|
325
256
|
}
|
|
326
257
|
async read() {
|
|
327
258
|
await this.waitUntilResumed();
|
|
328
259
|
if (this.closed) return null;
|
|
329
260
|
const reader = this.reader;
|
|
330
|
-
|
|
261
|
+
const abortController = this.abortController;
|
|
262
|
+
if (reader === void 0 || abortController === void 0) {
|
|
331
263
|
if (this.closed) return null;
|
|
332
|
-
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;
|
|
333
292
|
}
|
|
334
|
-
const result = await reader.read();
|
|
335
293
|
if (result.done) {
|
|
336
294
|
releaseReader(reader);
|
|
337
295
|
if (this.reader === reader) this.reader = void 0;
|
|
338
|
-
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
|
+
});
|
|
339
301
|
}
|
|
340
302
|
const bytes = result.value;
|
|
341
303
|
const receivedAtMs = this.now();
|
|
@@ -350,12 +312,14 @@ var HttpFlvLoader = class {
|
|
|
350
312
|
};
|
|
351
313
|
}
|
|
352
314
|
pause() {
|
|
353
|
-
if (this.closed) return;
|
|
315
|
+
if (this.closed || this.pausedState) return;
|
|
354
316
|
this.pausedState = true;
|
|
317
|
+
this.activeReadTimeout?.pause();
|
|
355
318
|
}
|
|
356
319
|
resume() {
|
|
357
320
|
if (!this.pausedState) return;
|
|
358
321
|
this.pausedState = false;
|
|
322
|
+
this.activeReadTimeout?.resume();
|
|
359
323
|
this.resolveResumeWaiters();
|
|
360
324
|
}
|
|
361
325
|
async close() {
|
|
@@ -363,6 +327,7 @@ var HttpFlvLoader = class {
|
|
|
363
327
|
this.state = "closed";
|
|
364
328
|
this.pausedState = false;
|
|
365
329
|
this.resolveResumeWaiters();
|
|
330
|
+
this.activeReadTimeout?.cancel();
|
|
366
331
|
this.abortController?.abort();
|
|
367
332
|
const reader = this.reader;
|
|
368
333
|
this.reader = void 0;
|
|
@@ -373,32 +338,6 @@ var HttpFlvLoader = class {
|
|
|
373
338
|
releaseReader(reader);
|
|
374
339
|
}
|
|
375
340
|
}
|
|
376
|
-
async openAttempt() {
|
|
377
|
-
const abortController = this.abortController;
|
|
378
|
-
if (abortController === void 0) throw new HttpFlvLoaderError("RIVMUX_LOADER_INVALID_STATE", "HTTP Fetch loader abort controller is missing.");
|
|
379
|
-
const response = await this.fetchImpl(this.url, {
|
|
380
|
-
method: "GET",
|
|
381
|
-
headers: createHeaders(this.headers),
|
|
382
|
-
credentials: this.credentials,
|
|
383
|
-
signal: abortController.signal
|
|
384
|
-
});
|
|
385
|
-
if (this.closed) {
|
|
386
|
-
await response.body?.cancel();
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (!response.ok) {
|
|
390
|
-
await response.body?.cancel();
|
|
391
|
-
throw new HttpFlvLoaderError("RIVMUX_HTTP_STATUS", `HTTP Fetch loader received status ${response.status} ${response.statusText}.`, response.status);
|
|
392
|
-
}
|
|
393
|
-
if (response.body === null) throw new HttpFlvLoaderError("RIVMUX_HTTP_BODY_UNAVAILABLE", "HTTP Fetch loader response body is unavailable.");
|
|
394
|
-
const contentLength = response.headers.get("Content-Length");
|
|
395
|
-
if (contentLength !== null) {
|
|
396
|
-
const parsedContentLength = Number.parseInt(contentLength, 10);
|
|
397
|
-
if (Number.isFinite(parsedContentLength) && parsedContentLength >= 0) this.mutableStats.contentLength = parsedContentLength;
|
|
398
|
-
}
|
|
399
|
-
this.reader = response.body.getReader();
|
|
400
|
-
this.state = "open";
|
|
401
|
-
}
|
|
402
341
|
waitUntilResumed() {
|
|
403
342
|
if (!this.pausedState || this.closed) return Promise.resolve();
|
|
404
343
|
return new Promise((resolve) => {
|
|
@@ -419,32 +358,198 @@ function createHeaders(headers) {
|
|
|
419
358
|
for (const [key, value] of Object.entries(headers)) result.append(key, value);
|
|
420
359
|
return result;
|
|
421
360
|
}
|
|
361
|
+
async function cancelResponseBody(response) {
|
|
362
|
+
try {
|
|
363
|
+
await response.body?.cancel();
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
422
366
|
function releaseReader(reader) {
|
|
423
367
|
try {
|
|
424
368
|
reader.releaseLock();
|
|
425
369
|
} catch {}
|
|
426
370
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
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
|
+
}
|
|
430
379
|
return new Promise((resolve, reject) => {
|
|
431
|
-
|
|
432
|
-
|
|
380
|
+
let settled = false;
|
|
381
|
+
const onAbort = () => {
|
|
382
|
+
if (settled) return;
|
|
383
|
+
settled = true;
|
|
433
384
|
signal.removeEventListener("abort", onAbort);
|
|
385
|
+
operation.then(onLateValue, () => void 0);
|
|
386
|
+
reject(createAbortError());
|
|
434
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;
|
|
435
492
|
const onAbort = () => {
|
|
436
|
-
|
|
437
|
-
|
|
493
|
+
if (settled) return;
|
|
494
|
+
settled = true;
|
|
495
|
+
signal.removeEventListener("abort", onAbort);
|
|
496
|
+
operation.then(onLateValue, () => void 0);
|
|
497
|
+
resolve({ cancelled: true });
|
|
438
498
|
};
|
|
439
|
-
const timer = setTimeout(() => {
|
|
440
|
-
cleanup();
|
|
441
|
-
resolve();
|
|
442
|
-
}, ms);
|
|
443
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
|
+
});
|
|
444
514
|
});
|
|
445
515
|
}
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
};
|
|
448
553
|
}
|
|
449
554
|
//#endregion
|
|
450
555
|
//#region src/mse/mime.ts
|
|
@@ -974,10 +1079,10 @@ function __wbg_get_imports() {
|
|
|
974
1079
|
__proto__: null,
|
|
975
1080
|
"./rivmux_transmux_core_bg.js": {
|
|
976
1081
|
__proto__: null,
|
|
977
|
-
|
|
1082
|
+
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
|
978
1083
|
return Error(getStringFromWasm0(arg0, arg1));
|
|
979
1084
|
},
|
|
980
|
-
|
|
1085
|
+
__wbg_Number_c54e7112a3fa7e3e: function(arg0) {
|
|
981
1086
|
return Number(arg0);
|
|
982
1087
|
},
|
|
983
1088
|
__wbg_String_8564e559799eccda: function(arg0, arg1) {
|
|
@@ -986,28 +1091,28 @@ function __wbg_get_imports() {
|
|
|
986
1091
|
getDataViewMemory0().setInt32(arg0 + 4, len1, true);
|
|
987
1092
|
getDataViewMemory0().setInt32(arg0 + 0, ptr1, true);
|
|
988
1093
|
},
|
|
989
|
-
|
|
1094
|
+
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
|
990
1095
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
991
1096
|
},
|
|
992
|
-
|
|
1097
|
+
__wbg_new_bebc3f4757acf305: function() {
|
|
993
1098
|
return /* @__PURE__ */ new Object();
|
|
994
1099
|
},
|
|
995
|
-
|
|
1100
|
+
__wbg_new_ffa92086ea89f79c: function() {
|
|
996
1101
|
return new Array();
|
|
997
1102
|
},
|
|
1103
|
+
__wbg_set_13d25b81ab403f5e: function(arg0, arg1, arg2) {
|
|
1104
|
+
arg0[arg1 >>> 0] = arg2;
|
|
1105
|
+
},
|
|
998
1106
|
__wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
|
|
999
1107
|
arg0[arg1] = arg2;
|
|
1000
1108
|
},
|
|
1001
|
-
|
|
1002
|
-
arg0[arg1 >>> 0] = arg2;
|
|
1003
|
-
},
|
|
1004
|
-
__wbindgen_cast_0000000000000001: function(arg0) {
|
|
1109
|
+
__wbindgen_generic_0000000000000001: function(arg0) {
|
|
1005
1110
|
return arg0;
|
|
1006
1111
|
},
|
|
1007
|
-
|
|
1112
|
+
__wbindgen_generic_0000000000000002: function(arg0) {
|
|
1008
1113
|
return arg0;
|
|
1009
1114
|
},
|
|
1010
|
-
|
|
1115
|
+
__wbindgen_generic_0000000000000003: function(arg0, arg1) {
|
|
1011
1116
|
return getStringFromWasm0(arg0, arg1);
|
|
1012
1117
|
},
|
|
1013
1118
|
__wbindgen_init_externref_table: function() {
|
|
@@ -1117,10 +1222,11 @@ function __wbg_finalize_init(instance, module) {
|
|
|
1117
1222
|
}
|
|
1118
1223
|
async function __wbg_load(module, imports) {
|
|
1119
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}'`);
|
|
1120
1226
|
if (typeof WebAssembly.instantiateStreaming === "function") try {
|
|
1121
1227
|
return await WebAssembly.instantiateStreaming(module, imports);
|
|
1122
1228
|
} catch (e) {
|
|
1123
|
-
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);
|
|
1124
1230
|
else throw e;
|
|
1125
1231
|
}
|
|
1126
1232
|
const bytes = await module.arrayBuffer();
|
|
@@ -1144,8 +1250,10 @@ async function __wbg_load(module, imports) {
|
|
|
1144
1250
|
}
|
|
1145
1251
|
async function __wbg_init(module_or_path) {
|
|
1146
1252
|
if (wasm !== void 0) return wasm;
|
|
1147
|
-
if (module_or_path !== void 0)
|
|
1148
|
-
|
|
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
|
+
}
|
|
1149
1257
|
if (module_or_path === void 0) module_or_path = new URL("rivmux_transmux_core_bg.wasm", import.meta.url);
|
|
1150
1258
|
const imports = __wbg_get_imports();
|
|
1151
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);
|
|
@@ -1470,7 +1578,14 @@ var RuntimeSession = class {
|
|
|
1470
1578
|
await this.dependencies.applyLatencyPolicy(context);
|
|
1471
1579
|
if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
|
|
1472
1580
|
const chunk = await loader.read();
|
|
1473
|
-
if (chunk === null
|
|
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;
|
|
1474
1589
|
this.dependencies.onStats(loader.stats);
|
|
1475
1590
|
if (!await this.processEvents(this.pushChunk(chunk.bytes), context)) {
|
|
1476
1591
|
await this.closeCurrentLoader(loader, runId);
|
|
@@ -1480,16 +1595,16 @@ var RuntimeSession = class {
|
|
|
1480
1595
|
if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
|
|
1481
1596
|
this.dependencies.onStats(loader.stats);
|
|
1482
1597
|
}
|
|
1483
|
-
if (this.isCurrentLoader(loader, runId) && this.isLifecycleContextCurrent(context)) {
|
|
1484
|
-
if (!await this.appendController.flush(context)) return;
|
|
1485
|
-
this.dependencies.onStats(loader.stats);
|
|
1486
|
-
}
|
|
1487
1598
|
} catch (cause) {
|
|
1488
1599
|
if (!this.isCurrentLoader(loader, runId) || isAbortLikeError(cause)) return;
|
|
1489
1600
|
try {
|
|
1490
1601
|
await this.closeCurrentLoader(loader, runId);
|
|
1491
1602
|
} catch {}
|
|
1492
1603
|
if (!this.isLifecycleContextCurrent(context)) return;
|
|
1604
|
+
if (isRecoverableLoaderError(cause)) {
|
|
1605
|
+
this.dependencies.onRecoverableFailure(cause, context);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1493
1608
|
const code = cause instanceof HttpFlvLoaderError ? cause.code : "RIVMUX_HTTP_LOADER_FAILED";
|
|
1494
1609
|
this.dependencies.onFailure("network", code, "HTTP Fetch loader failed.", cause);
|
|
1495
1610
|
} finally {
|
|
@@ -1531,11 +1646,6 @@ var RuntimeSession = class {
|
|
|
1531
1646
|
if (append !== void 0 && !await append) return false;
|
|
1532
1647
|
break;
|
|
1533
1648
|
}
|
|
1534
|
-
case "probeResult":
|
|
1535
|
-
case "trackConfig":
|
|
1536
|
-
case "sample":
|
|
1537
|
-
case "metadata":
|
|
1538
|
-
case "discontinuity": break;
|
|
1539
1649
|
}
|
|
1540
1650
|
}
|
|
1541
1651
|
return this.appendController.waitForTail();
|
|
@@ -1583,6 +1693,8 @@ var RuntimeWorker = class {
|
|
|
1583
1693
|
port;
|
|
1584
1694
|
detectRuntime;
|
|
1585
1695
|
now;
|
|
1696
|
+
sleep;
|
|
1697
|
+
random;
|
|
1586
1698
|
state = "idle";
|
|
1587
1699
|
url;
|
|
1588
1700
|
options;
|
|
@@ -1598,21 +1710,31 @@ var RuntimeWorker = class {
|
|
|
1598
1710
|
lifecycleGeneration = 0;
|
|
1599
1711
|
lifecycleAbortController = new AbortController();
|
|
1600
1712
|
fatalCleanupPromise = Promise.resolve();
|
|
1713
|
+
recoveryPromise;
|
|
1714
|
+
connectionAttempt = 1;
|
|
1715
|
+
recoveryStartedAt;
|
|
1716
|
+
pendingRecovery;
|
|
1601
1717
|
constructor(port, dependencies = {}) {
|
|
1602
1718
|
this.port = port;
|
|
1603
1719
|
this.detectRuntime = dependencies.detectRuntime ?? detectWorkerRuntime;
|
|
1604
1720
|
this.now = dependencies.now ?? (() => performance.now());
|
|
1721
|
+
this.sleep = dependencies.sleep ?? waitForDelay;
|
|
1722
|
+
this.random = dependencies.random ?? Math.random;
|
|
1605
1723
|
this.session = new RuntimeSession({
|
|
1606
1724
|
...dependencies,
|
|
1607
1725
|
isStarted: () => this.state === "started",
|
|
1608
1726
|
isLifecycleContextCurrent: (context) => this.isLifecycleContextCurrent(context),
|
|
1609
|
-
onMediaAppended: (context) =>
|
|
1727
|
+
onMediaAppended: async (context) => {
|
|
1728
|
+
this.markRecovered(context);
|
|
1729
|
+
await this.applyLatencyPolicy(context);
|
|
1730
|
+
},
|
|
1610
1731
|
onAppendError: (cause, context) => {
|
|
1611
1732
|
if (this.isLifecycleContextCurrent(context)) this.fail("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", true, cause);
|
|
1612
1733
|
},
|
|
1613
1734
|
onLoaderClosing: () => this.stopStatsTimer(),
|
|
1614
1735
|
onStats: (stats) => this.postStats(stats),
|
|
1615
1736
|
onMessage: (message) => this.post(message),
|
|
1737
|
+
onRecoverableFailure: (cause, context) => this.scheduleRecovery(cause, context),
|
|
1616
1738
|
onFailure: (kind, code, message, cause) => this.fail(kind, code, message, true, cause),
|
|
1617
1739
|
onPlayerError: (error) => this.failWithError(error),
|
|
1618
1740
|
applyLatencyPolicy: (context) => this.applyLatencyPolicy(context),
|
|
@@ -1661,10 +1783,6 @@ var RuntimeWorker = class {
|
|
|
1661
1783
|
case "stop":
|
|
1662
1784
|
await this.stop();
|
|
1663
1785
|
return;
|
|
1664
|
-
case "update-options":
|
|
1665
|
-
this.options = this.options === void 0 ? void 0 : mergeOptions(this.options, command.options);
|
|
1666
|
-
if (this.options !== void 0) this.latencyController = createLatencyController(this.options);
|
|
1667
|
-
return;
|
|
1668
1786
|
case "video-state":
|
|
1669
1787
|
this.videoState = command.state;
|
|
1670
1788
|
if (!(await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled && this.isLifecycleContextCurrent(context) && this.state === "started") this.postStats();
|
|
@@ -1704,7 +1822,10 @@ var RuntimeWorker = class {
|
|
|
1704
1822
|
this.fail("runtime", "RIVMUX_WORKER_START_REQUIRES_ATTACH", "Worker start requires an attached MediaSource.", true);
|
|
1705
1823
|
return;
|
|
1706
1824
|
}
|
|
1707
|
-
if (this.state === "started")
|
|
1825
|
+
if (this.state === "started") {
|
|
1826
|
+
this.post({ type: "started" });
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1708
1829
|
try {
|
|
1709
1830
|
const transmuxCore = await this.session.createCore(options, context);
|
|
1710
1831
|
if (context.generation !== this.lifecycleGeneration) return;
|
|
@@ -1724,24 +1845,30 @@ var RuntimeWorker = class {
|
|
|
1724
1845
|
if ((await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return;
|
|
1725
1846
|
this.startStatsTimer();
|
|
1726
1847
|
this.postStats();
|
|
1727
|
-
this.startLoader(context);
|
|
1848
|
+
if (!this.startLoader(context)) return;
|
|
1849
|
+
if (!this.isLifecycleContextCurrent(context) || this.state !== "started") return;
|
|
1850
|
+
this.post({ type: "started" });
|
|
1728
1851
|
}
|
|
1729
1852
|
async stop() {
|
|
1853
|
+
await this.waitForRecovery();
|
|
1730
1854
|
await this.closeLoader();
|
|
1731
1855
|
this.session.destroyMse();
|
|
1732
1856
|
this.latencyController?.reset();
|
|
1733
1857
|
this.videoState = void 0;
|
|
1734
1858
|
this.lastLatencyMetrics = {};
|
|
1859
|
+
this.resetRecoveryCycle();
|
|
1735
1860
|
this.state = "stopped";
|
|
1736
1861
|
this.post({ type: "stopped" });
|
|
1737
1862
|
}
|
|
1738
1863
|
async destroy() {
|
|
1739
1864
|
await this.fatalCleanupPromise;
|
|
1865
|
+
await this.waitForRecovery();
|
|
1740
1866
|
await this.closeLoader();
|
|
1741
1867
|
this.session.destroyMse();
|
|
1742
1868
|
this.latencyController?.reset();
|
|
1743
1869
|
this.videoState = void 0;
|
|
1744
1870
|
this.lastLatencyMetrics = {};
|
|
1871
|
+
this.resetRecoveryCycle();
|
|
1745
1872
|
this.state = "destroyed";
|
|
1746
1873
|
this.post({ type: "destroyed" });
|
|
1747
1874
|
this.port.close();
|
|
@@ -1751,17 +1878,117 @@ var RuntimeWorker = class {
|
|
|
1751
1878
|
const url = this.url;
|
|
1752
1879
|
if (options === void 0 || url === void 0) {
|
|
1753
1880
|
this.fail("runtime", "RIVMUX_WORKER_NOT_INITIALIZED", "Worker must be initialized before loader start.", true);
|
|
1754
|
-
return;
|
|
1881
|
+
return false;
|
|
1755
1882
|
}
|
|
1756
1883
|
this.session.runLoader({
|
|
1757
1884
|
url,
|
|
1758
1885
|
network: options.network
|
|
1759
1886
|
}, context);
|
|
1887
|
+
return true;
|
|
1760
1888
|
}
|
|
1761
1889
|
async closeLoader() {
|
|
1762
1890
|
this.stopStatsTimer();
|
|
1763
1891
|
await this.session.close();
|
|
1764
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;
|
|
1897
|
+
});
|
|
1898
|
+
this.recoveryPromise = recovery;
|
|
1899
|
+
}
|
|
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;
|
|
1926
|
+
try {
|
|
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;
|
|
1950
|
+
}
|
|
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);
|
|
1954
|
+
return;
|
|
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
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
async waitForRecovery() {
|
|
1983
|
+
try {
|
|
1984
|
+
await this.recoveryPromise;
|
|
1985
|
+
} catch {}
|
|
1986
|
+
}
|
|
1987
|
+
resetRecoveryCycle() {
|
|
1988
|
+
this.connectionAttempt = 1;
|
|
1989
|
+
this.recoveryStartedAt = void 0;
|
|
1990
|
+
this.pendingRecovery = void 0;
|
|
1991
|
+
}
|
|
1765
1992
|
postStats(loaderStats) {
|
|
1766
1993
|
const mseStats = this.collectMseStats();
|
|
1767
1994
|
const loaderSnapshot = loaderStats ?? this.session.loaderStats;
|
|
@@ -1884,6 +2111,7 @@ var RuntimeWorker = class {
|
|
|
1884
2111
|
this.latencyController?.reset();
|
|
1885
2112
|
this.videoState = void 0;
|
|
1886
2113
|
this.lastLatencyMetrics = {};
|
|
2114
|
+
this.resetRecoveryCycle();
|
|
1887
2115
|
}
|
|
1888
2116
|
post(message, transfer) {
|
|
1889
2117
|
this.port.postMessage(message, transfer);
|
|
@@ -1894,12 +2122,41 @@ var RuntimeWorker = class {
|
|
|
1894
2122
|
this.lifecycleAbortController = new AbortController();
|
|
1895
2123
|
}
|
|
1896
2124
|
};
|
|
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
|
+
});
|
|
2143
|
+
}
|
|
1897
2144
|
function serializeCause(cause) {
|
|
1898
2145
|
if (cause instanceof Error) return {
|
|
1899
2146
|
name: cause.name,
|
|
1900
2147
|
message: cause.message
|
|
1901
2148
|
};
|
|
1902
|
-
|
|
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
|
+
};
|
|
1903
2160
|
}
|
|
1904
2161
|
function detectWorkerRuntime() {
|
|
1905
2162
|
if (typeof fetch !== "function") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_FETCH", "Fetch is not available in this worker runtime.");
|