@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.
@@ -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
- constructor(code, message, status) {
165
- super(message);
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.status = status;
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
- retry = createRetryPolicy(void 0);
171
+ readIdleTimeoutMs;
176
172
  fetchImpl;
177
173
  now;
178
- sleep;
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.retry = createRetryPolicy(config.network.retry);
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.sleep = config.sleep ?? wait;
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
- for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt += 1) {
211
- this.abortController = new AbortController();
212
- try {
213
- await this.openAttempt();
214
- return;
215
- } catch (cause) {
216
- if (this.closed || isAbortLikeError(cause) || attempt >= this.retry.maxAttempts) throw cause;
217
- await this.sleep(getRetryDelayMs(this.retry, attempt), this.abortController.signal);
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
- if (reader === void 0) {
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 wait(ms, signal) {
323
- if (ms <= 0) return Promise.resolve();
324
- if (signal.aborted) return Promise.reject(createAbortError());
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
- const cleanup = () => {
327
- clearTimeout(timer);
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
- cleanup();
332
- reject(createAbortError());
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
- function createAbortError() {
342
- return new DOMException("HTTP Fetch loader was aborted.", "AbortError");
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
- const REQUIRED_MSE_MIME_TYPES = [{
353
- mediaType: "video",
354
- mimeType: createMp4VideoMime("avc1.42C01E"),
355
- unsupportedCode: "RIVMUX_UNSUPPORTED_MSE_VIDEO_MIME"
356
- }, {
357
- mediaType: "audio",
358
- mimeType: createMp4AudioMime("mp4a.40.2"),
359
- unsupportedCode: "RIVMUX_UNSUPPORTED_MSE_AUDIO_MIME"
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 assertMseSupport(mimeType) {
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 (!isMseSupported(mimeType)) throw new Error(`MSE does not support ${mimeType}.`);
576
+ if (typeof MediaSource.isTypeSupported !== "function") throw new Error("MediaSource.isTypeSupported is not available in this worker.");
368
577
  }
369
- function assertRequiredMseSupport() {
370
- for (const requirement of REQUIRED_MSE_MIME_TYPES) assertMseSupport(requirement.mimeType);
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
- assertRequiredMseSupport();
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 "videoConfig":
681
- case "audioConfig":
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
- try {
844
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
845
- wasm.transmuxcore_flush(retptr, this.__wbg_ptr);
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
- try {
866
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
867
- const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
868
- const len0 = WASM_VECTOR_LEN;
869
- wasm.transmuxcore_pushChunk(retptr, this.__wbg_ptr, ptr0, len0);
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
- __wbg_Error_fdd633d4bb5dd76a: function(arg0, arg1) {
889
- return addHeapObject(Error(getStringFromWasm0(arg0, arg1)));
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(getObject(arg1)), wasm.__wbindgen_export, wasm.__wbindgen_export2);
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
- __wbg___wbindgen_throw_ea4887a5f8f9a9db: function(arg0, arg1) {
1094
+ __wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
898
1095
  throw new Error(getStringFromWasm0(arg0, arg1));
899
1096
  },
900
- __wbg_new_2e117a478906f062: function() {
901
- return addHeapObject(/* @__PURE__ */ new Object());
902
- },
903
- __wbg_new_36e147a8ced3c6e0: function() {
904
- return addHeapObject(new Array());
1097
+ __wbg_new_bebc3f4757acf305: function() {
1098
+ return /* @__PURE__ */ new Object();
905
1099
  },
906
- __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
907
- getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
1100
+ __wbg_new_ffa92086ea89f79c: function() {
1101
+ return new Array();
908
1102
  },
909
- __wbg_set_dc601f4a69da0bc2: function(arg0, arg1, arg2) {
910
- getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
1103
+ __wbg_set_13d25b81ab403f5e: function(arg0, arg1, arg2) {
1104
+ arg0[arg1 >>> 0] = arg2;
911
1105
  },
912
- __wbindgen_cast_0000000000000001: function(arg0) {
913
- return addHeapObject(arg0);
1106
+ __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
1107
+ arg0[arg1] = arg2;
914
1108
  },
915
- __wbindgen_cast_0000000000000002: function(arg0) {
916
- return addHeapObject(arg0);
1109
+ __wbindgen_generic_0000000000000001: function(arg0) {
1110
+ return arg0;
917
1111
  },
918
- __wbindgen_cast_0000000000000003: function(arg0, arg1) {
919
- return addHeapObject(getStringFromWasm0(arg0, arg1));
1112
+ __wbindgen_generic_0000000000000002: function(arg0) {
1113
+ return arg0;
920
1114
  },
921
- __wbindgen_object_clone_ref: function(arg0) {
922
- return addHeapObject(getObject(arg0));
1115
+ __wbindgen_generic_0000000000000003: function(arg0, arg1) {
1116
+ return getStringFromWasm0(arg0, arg1);
923
1117
  },
924
- __wbindgen_object_drop_ref: function(arg0) {
925
- takeObject(arg0);
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 takeObject(idx) {
1000
- const ret = getObject(idx);
1001
- dropObject(idx);
1002
- return ret;
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 (module.ok && 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);
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) if (Object.getPrototypeOf(module_or_path) === Object.prototype) ({module_or_path} = module_or_path);
1070
- else console.warn("using deprecated parameters for the initialization function; pass a single object instead");
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
- var RuntimeWorker = class {
1090
- port;
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
- mse;
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 handleCommand(command) {
1754
+ async executeCommand(command, context) {
1120
1755
  if (this.state === "destroyed") return;
1121
- if (this.state === "fatal-error" && command.type !== "destroy") return;
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.mse.createMediaSourceHandle();
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.mse === void 0 || options === void 0 || this.state === "idle" || this.state === "ready") {
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") return;
1191
- let transmuxCore;
1825
+ if (this.state === "started") {
1826
+ this.post({ type: "started" });
1827
+ return;
1828
+ }
1192
1829
  try {
1193
- const createdCore = await this.createTransmuxCore(options);
1194
- if (createdCore === void 0) {
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 = createdCore;
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.mse?.destroy();
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.mse?.destroy();
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
- const loader = this.createLoader({
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
- const runId = this.loaderRunId + 1;
1247
- this.loaderRunId = runId;
1248
- this.loader = loader;
1249
- this.runLoader(loader, runId);
1898
+ this.recoveryPromise = recovery;
1250
1899
  }
1251
- async runLoader(loader, runId) {
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 loader.open();
1254
- while (this.isCurrentLoader(loader, runId)) {
1255
- await this.applyLatencyPolicy();
1256
- const chunk = await loader.read();
1257
- if (chunk === null) break;
1258
- this.postStats(loader.stats);
1259
- if (!await this.processTransmuxEvents(this.transmuxCore?.pushChunk(chunk.bytes) ?? [])) {
1260
- await this.closeCurrentLoader(loader, runId);
1261
- return;
1262
- }
1263
- await this.applyLatencyPolicy();
1264
- this.postStats(loader.stats);
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
- } catch (cause) {
1267
- if (!this.isCurrentLoader(loader, runId) || isAbortLikeError(cause)) return;
1268
- await this.closeCurrentLoader(loader, runId);
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 closeLoader() {
1276
- this.stopStatsTimer();
1277
- const loader = this.loader;
1278
- if (loader === void 0) return;
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
- isCurrentLoader(loader, runId) {
1295
- return this.loader === loader && this.loaderRunId === runId && this.state === "started";
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.loader?.stats;
1994
+ const loaderSnapshot = loaderStats ?? this.session.loaderStats;
1301
1995
  this.post({
1302
1996
  type: "stats",
1303
- stats: {
1304
- bytesReceived: loaderSnapshot?.bytesReceived ?? 0,
1305
- currentNetworkSpeed: loaderSnapshot?.currentNetworkSpeed ?? 0,
1306
- networkIdleMs: getNetworkIdleMs(loaderSnapshot, this.now()),
1307
- outputBytes: this.outputBytes,
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.loader?.paused ?? false,
1313
- sourceBufferUpdating: mseStats.sourceBufferUpdating,
1314
- sourceBufferCount: mseStats.sourceBufferCount,
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 appendQueueLength = this.mse?.appendQueueLength ?? 0;
1366
- const appendQueueBytes = this.mse?.appendQueueBytes ?? 0;
1367
- this.appendQueueMaxLength = Math.max(this.appendQueueMaxLength, appendQueueLength);
1368
- this.appendQueueMaxBytes = Math.max(this.appendQueueMaxBytes, appendQueueBytes);
1369
- return {
1370
- appendQueueLength,
1371
- appendQueueBytes,
1372
- sourceBufferUpdating: this.mse?.sourceBufferUpdating ?? false,
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.mse?.bufferedEnd;
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
- const mse = this.mse;
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: mse.bufferedRanges,
2030
+ ranges: this.session.bufferedRanges,
1420
2031
  videoState: this.videoState,
1421
- loaderPaused: loader?.paused ?? false,
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 mse.cleanupBefore(evaluation.cleanupBefore);
1426
- if (loader !== void 0 && evaluation.loaderCommand === "pause") loader.pause();
1427
- else if (loader !== void 0 && evaluation.loaderCommand === "resume") loader.resume();
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.closeLoader();
1485
- this.mse?.destroy();
1486
- this.mse = void 0;
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 getNetworkErrorCode(cause) {
1496
- return cause instanceof HttpFlvLoaderError ? cause.code : "RIVMUX_HTTP_LOADER_FAILED";
1497
- }
1498
- function getNetworkIdleMs(stats, nowMs) {
1499
- const markerMs = stats?.lastChunkAtMs ?? stats?.startedAtMs;
1500
- if (markerMs === void 0) return;
1501
- return Math.max(nowMs - markerMs, 0);
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
- return cause;
1509
- }
1510
- function isQuotaExceededError(cause) {
1511
- return isNamedError(cause, "QuotaExceededError");
1512
- }
1513
- function isNamedError(value, name) {
1514
- return typeof value === "object" && value !== null && "name" in value && value.name === name;
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) {