@rivmux/runtime-worker 0.4.0 → 0.5.0

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,6 +141,111 @@ 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
144
249
  //#region src/loader/retry-policy.ts
145
250
  function createRetryPolicy(input) {
146
251
  return {
@@ -349,25 +454,25 @@ function createMp4VideoMime(codec) {
349
454
  function createMp4AudioMime(codec) {
350
455
  return `audio/mp4; codecs="${codec}"`;
351
456
  }
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
- }];
457
+ var MseUnsupportedMimeError = class extends Error {
458
+ mimeType;
459
+ constructor(mimeType) {
460
+ super(`MSE does not support ${mimeType}.`);
461
+ this.name = "MseUnsupportedMimeError";
462
+ this.mimeType = mimeType;
463
+ }
464
+ };
361
465
  function isMseSupported(mimeType) {
362
466
  return typeof MediaSource !== "undefined" && typeof MediaSource.isTypeSupported === "function" && MediaSource.isTypeSupported(mimeType);
363
467
  }
364
- function assertMseSupport(mimeType) {
468
+ function assertMseRuntimeSupport() {
365
469
  if (typeof MediaSource === "undefined") throw new Error("MediaSource is not available in this worker.");
366
470
  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}.`);
471
+ if (typeof MediaSource.isTypeSupported !== "function") throw new Error("MediaSource.isTypeSupported is not available in this worker.");
368
472
  }
369
- function assertRequiredMseSupport() {
370
- for (const requirement of REQUIRED_MSE_MIME_TYPES) assertMseSupport(requirement.mimeType);
473
+ function assertMseSupport(mimeType) {
474
+ assertMseRuntimeSupport();
475
+ if (!isMseSupported(mimeType)) throw new MseUnsupportedMimeError(mimeType);
371
476
  }
372
477
  //#endregion
373
478
  //#region src/mse/source-buffer-queue.ts
@@ -518,7 +623,7 @@ var MseController = class {
518
623
  return Array.from(this.queues.values()).reduce((total, queue) => total + queue.bufferedRanges.length, 0);
519
624
  }
520
625
  async createMediaSourceHandle() {
521
- assertRequiredMseSupport();
626
+ assertMseRuntimeSupport();
522
627
  const mediaSource = new MediaSource();
523
628
  this.mediaSource = mediaSource;
524
629
  const handle = mediaSource.handle;
@@ -533,7 +638,7 @@ var MseController = class {
533
638
  await this.ensureQueue(segment.track, mimeType).append(toAppendBuffer(segment.bytes));
534
639
  }
535
640
  async appendMediaSegment(segment) {
536
- const queue = this.queues.get(segment.track);
641
+ const queue = this.queues.get(segment.track) ?? this.queues.get("muxed");
537
642
  if (queue === void 0) throw new Error(`Cannot append ${segment.track} media segment before init segment.`);
538
643
  await queue.append(toAppendBuffer(segment.bytes));
539
644
  const mediaSource = this.requireMediaSource();
@@ -677,10 +782,8 @@ function normalizeCoreEvent(value) {
677
782
  type: "mediaSegment",
678
783
  data: normalizeMediaSegment(data)
679
784
  };
680
- case "videoConfig":
681
- case "audioConfig":
682
- case "videoSample":
683
- case "audioSample":
785
+ case "trackConfig":
786
+ case "sample":
684
787
  case "metadata":
685
788
  case "discontinuity": return {
686
789
  type: value.type,
@@ -840,16 +943,9 @@ var TransmuxCore = class {
840
943
  * @returns {any}
841
944
  */
842
945
  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
- }
946
+ const ret = wasm.transmuxcore_flush(this.__wbg_ptr);
947
+ if (ret[2]) throw takeFromExternrefTable0(ret[1]);
948
+ return takeFromExternrefTable0(ret[0]);
853
949
  }
854
950
  constructor() {
855
951
  const ret = wasm.transmuxcore_new();
@@ -862,18 +958,11 @@ var TransmuxCore = class {
862
958
  * @returns {any}
863
959
  */
864
960
  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
- }
961
+ const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
962
+ const len0 = WASM_VECTOR_LEN;
963
+ const ret = wasm.transmuxcore_pushChunk(this.__wbg_ptr, ptr0, len0);
964
+ if (ret[2]) throw takeFromExternrefTable0(ret[1]);
965
+ return takeFromExternrefTable0(ret[0]);
877
966
  }
878
967
  reset() {
879
968
  wasm.transmuxcore_reset(this.__wbg_ptr);
@@ -886,10 +975,13 @@ function __wbg_get_imports() {
886
975
  "./rivmux_transmux_core_bg.js": {
887
976
  __proto__: null,
888
977
  __wbg_Error_fdd633d4bb5dd76a: function(arg0, arg1) {
889
- return addHeapObject(Error(getStringFromWasm0(arg0, arg1)));
978
+ return Error(getStringFromWasm0(arg0, arg1));
979
+ },
980
+ __wbg_Number_c4bdf66bb78f7977: function(arg0) {
981
+ return Number(arg0);
890
982
  },
891
983
  __wbg_String_8564e559799eccda: function(arg0, arg1) {
892
- const ptr1 = passStringToWasm0(String(getObject(arg1)), wasm.__wbindgen_export, wasm.__wbindgen_export2);
984
+ const ptr1 = passStringToWasm0(String(arg1), wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
893
985
  const len1 = WASM_VECTOR_LEN;
894
986
  getDataViewMemory0().setInt32(arg0 + 4, len1, true);
895
987
  getDataViewMemory0().setInt32(arg0 + 0, ptr1, true);
@@ -898,31 +990,34 @@ function __wbg_get_imports() {
898
990
  throw new Error(getStringFromWasm0(arg0, arg1));
899
991
  },
900
992
  __wbg_new_2e117a478906f062: function() {
901
- return addHeapObject(/* @__PURE__ */ new Object());
993
+ return /* @__PURE__ */ new Object();
902
994
  },
903
995
  __wbg_new_36e147a8ced3c6e0: function() {
904
- return addHeapObject(new Array());
996
+ return new Array();
905
997
  },
906
998
  __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
907
- getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
999
+ arg0[arg1] = arg2;
908
1000
  },
909
1001
  __wbg_set_dc601f4a69da0bc2: function(arg0, arg1, arg2) {
910
- getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
1002
+ arg0[arg1 >>> 0] = arg2;
911
1003
  },
912
1004
  __wbindgen_cast_0000000000000001: function(arg0) {
913
- return addHeapObject(arg0);
1005
+ return arg0;
914
1006
  },
915
1007
  __wbindgen_cast_0000000000000002: function(arg0) {
916
- return addHeapObject(arg0);
1008
+ return arg0;
917
1009
  },
918
1010
  __wbindgen_cast_0000000000000003: function(arg0, arg1) {
919
- return addHeapObject(getStringFromWasm0(arg0, arg1));
1011
+ return getStringFromWasm0(arg0, arg1);
920
1012
  },
921
- __wbindgen_object_clone_ref: function(arg0) {
922
- return addHeapObject(getObject(arg0));
923
- },
924
- __wbindgen_object_drop_ref: function(arg0) {
925
- takeObject(arg0);
1013
+ __wbindgen_init_externref_table: function() {
1014
+ const table = wasm.__wbindgen_externrefs;
1015
+ const offset = table.grow(4);
1016
+ table.set(0, void 0);
1017
+ table.set(offset + 0, void 0);
1018
+ table.set(offset + 1, null);
1019
+ table.set(offset + 2, true);
1020
+ table.set(offset + 3, false);
926
1021
  }
927
1022
  }
928
1023
  };
@@ -931,18 +1026,6 @@ const TransmuxCoreFinalization = typeof FinalizationRegistry === "undefined" ? {
931
1026
  register: () => {},
932
1027
  unregister: () => {}
933
1028
  } : 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
1029
  let cachedDataViewMemory0 = null;
947
1030
  function getDataViewMemory0() {
948
1031
  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 +1039,6 @@ function getUint8ArrayMemory0() {
956
1039
  if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
957
1040
  return cachedUint8ArrayMemory0;
958
1041
  }
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
1042
  function passArray8ToWasm0(arg, malloc) {
966
1043
  const ptr = malloc(arg.length * 1, 1) >>> 0;
967
1044
  getUint8ArrayMemory0().set(arg, ptr / 1);
@@ -996,10 +1073,10 @@ function passStringToWasm0(arg, malloc, realloc) {
996
1073
  WASM_VECTOR_LEN = offset;
997
1074
  return ptr;
998
1075
  }
999
- function takeObject(idx) {
1000
- const ret = getObject(idx);
1001
- dropObject(idx);
1002
- return ret;
1076
+ function takeFromExternrefTable0(idx) {
1077
+ const value = wasm.__wbindgen_externrefs.get(idx);
1078
+ wasm.__externref_table_dealloc(idx);
1079
+ return value;
1003
1080
  }
1004
1081
  let cachedTextDecoder = new TextDecoder("utf-8", {
1005
1082
  ignoreBOM: true,
@@ -1035,6 +1112,7 @@ function __wbg_finalize_init(instance, module) {
1035
1112
  wasm = instance.exports;
1036
1113
  cachedDataViewMemory0 = null;
1037
1114
  cachedUint8ArrayMemory0 = null;
1115
+ wasm.__wbindgen_start();
1038
1116
  return wasm;
1039
1117
  }
1040
1118
  async function __wbg_load(module, imports) {
@@ -1084,41 +1162,480 @@ async function loadWasmTransmuxCoreHost(wasmUrl) {
1084
1162
  await __wbg_init(wasmUrl ?? new URL("./rivmux-transmux-core.wasm", import.meta.url));
1085
1163
  return createWasmTransmuxCoreHost(TransmuxCore);
1086
1164
  }
1165
+ var Fmp4AppendBatcher = class {
1166
+ pending = /* @__PURE__ */ new Map();
1167
+ timers = /* @__PURE__ */ new Map();
1168
+ maxDurationMs;
1169
+ maxBytes;
1170
+ onFlushDue;
1171
+ constructor(onFlushDue, options = {}) {
1172
+ this.onFlushDue = onFlushDue;
1173
+ this.maxDurationMs = options.maxDurationMs ?? 125;
1174
+ this.maxBytes = options.maxBytes ?? 524288;
1175
+ }
1176
+ push(segment) {
1177
+ let batch = this.pending.get(segment.track);
1178
+ if (batch !== void 0 && this.wouldExceedLimit(batch, segment)) {
1179
+ const flushed = this.flush(segment.track);
1180
+ batch = this.createBatch(segment);
1181
+ this.pending.set(segment.track, batch);
1182
+ this.scheduleFlush(segment.track);
1183
+ return flushed;
1184
+ }
1185
+ if (batch === void 0) {
1186
+ batch = this.createBatch(segment);
1187
+ this.pending.set(segment.track, batch);
1188
+ this.scheduleFlush(segment.track);
1189
+ } else {
1190
+ batch.dtsEndMs = Math.max(batch.dtsEndMs, segment.dtsEndMs);
1191
+ batch.parts.push(segment.bytes);
1192
+ batch.byteLength += segment.bytes.byteLength;
1193
+ }
1194
+ return batch.byteLength >= this.maxBytes ? this.flush(segment.track) : void 0;
1195
+ }
1196
+ flush(track) {
1197
+ const batch = this.pending.get(track);
1198
+ if (batch === void 0) return;
1199
+ this.pending.delete(track);
1200
+ this.cancelFlush(track);
1201
+ return {
1202
+ track,
1203
+ dtsStartMs: batch.dtsStartMs,
1204
+ dtsEndMs: batch.dtsEndMs,
1205
+ keyframe: batch.keyframe,
1206
+ bytes: mergeBytes(batch.parts, batch.byteLength)
1207
+ };
1208
+ }
1209
+ flushAll() {
1210
+ const batches = [];
1211
+ for (const track of [...this.pending.keys()]) {
1212
+ const batch = this.flush(track);
1213
+ if (batch !== void 0) batches.push(batch);
1214
+ }
1215
+ return batches;
1216
+ }
1217
+ discard() {
1218
+ this.pending.clear();
1219
+ for (const timer of this.timers.values()) clearTimeout(timer);
1220
+ this.timers.clear();
1221
+ }
1222
+ createBatch(segment) {
1223
+ return {
1224
+ track: segment.track,
1225
+ dtsStartMs: segment.dtsStartMs,
1226
+ dtsEndMs: segment.dtsEndMs,
1227
+ keyframe: segment.keyframe,
1228
+ parts: [segment.bytes],
1229
+ byteLength: segment.bytes.byteLength
1230
+ };
1231
+ }
1232
+ wouldExceedLimit(batch, segment) {
1233
+ return Math.max(batch.dtsEndMs, segment.dtsEndMs) - batch.dtsStartMs > this.maxDurationMs || batch.byteLength + segment.bytes.byteLength > this.maxBytes;
1234
+ }
1235
+ scheduleFlush(track) {
1236
+ this.timers.set(track, setTimeout(() => {
1237
+ this.timers.delete(track);
1238
+ this.onFlushDue(track);
1239
+ }, this.maxDurationMs));
1240
+ }
1241
+ cancelFlush(track) {
1242
+ const timer = this.timers.get(track);
1243
+ if (timer !== void 0) {
1244
+ clearTimeout(timer);
1245
+ this.timers.delete(track);
1246
+ }
1247
+ }
1248
+ };
1249
+ function mergeBytes(parts, byteLength) {
1250
+ const bytes = new Uint8Array(byteLength);
1251
+ let offset = 0;
1252
+ for (const part of parts) {
1253
+ bytes.set(part, offset);
1254
+ offset += part.byteLength;
1255
+ }
1256
+ return bytes;
1257
+ }
1087
1258
  //#endregion
1088
- //#region src/runtime.ts
1089
- var RuntimeWorker = class {
1090
- port;
1259
+ //#region src/runtime/append.ts
1260
+ /** Owns fMP4 batching and serialized MSE media-segment appends. */
1261
+ var RuntimeAppendController = class {
1262
+ dependencies;
1263
+ batcher;
1264
+ generation = 0;
1265
+ tail = Promise.resolve(true);
1266
+ constructor(dependencies) {
1267
+ this.dependencies = dependencies;
1268
+ }
1269
+ start(context) {
1270
+ this.discard();
1271
+ this.batcher = new Fmp4AppendBatcher((track) => {
1272
+ const batch = this.batcher?.flush(track);
1273
+ if (batch !== void 0) this.enqueue(batch, context);
1274
+ });
1275
+ }
1276
+ push(segment, context) {
1277
+ const batch = this.batcher?.push(segment);
1278
+ return batch === void 0 ? void 0 : this.enqueue(batch, context);
1279
+ }
1280
+ async flush(context, track) {
1281
+ const batcher = this.batcher;
1282
+ if (batcher === void 0) return true;
1283
+ const batches = track === void 0 ? batcher.flushAll() : [batcher.flush(track)].filter((batch) => batch !== void 0);
1284
+ for (const batch of batches) if (!await this.enqueue(batch, context)) return false;
1285
+ return true;
1286
+ }
1287
+ async waitForTail() {
1288
+ return this.tail;
1289
+ }
1290
+ discard() {
1291
+ this.generation += 1;
1292
+ this.batcher?.discard();
1293
+ this.batcher = void 0;
1294
+ this.tail = Promise.resolve(true);
1295
+ }
1296
+ enqueue(segment, context) {
1297
+ const generation = this.generation;
1298
+ const append = this.tail.then(async (previousAppendSucceeded) => {
1299
+ if (!previousAppendSucceeded || generation !== this.generation || !this.dependencies.isStarted() || !this.dependencies.isLifecycleContextCurrent(context)) return false;
1300
+ if (!await this.dependencies.appendToMse(segment, context)) return false;
1301
+ if (!this.dependencies.isLifecycleContextCurrent(context)) return false;
1302
+ await this.dependencies.onAppended(segment, context);
1303
+ return true;
1304
+ });
1305
+ this.tail = append.catch((cause) => {
1306
+ if (this.dependencies.isLifecycleContextCurrent(context)) this.dependencies.onError(cause, context);
1307
+ return false;
1308
+ });
1309
+ return this.tail;
1310
+ }
1311
+ };
1312
+ //#endregion
1313
+ //#region src/runtime/session.ts
1314
+ /** Owns all resources that exist only for an attached playback session. */
1315
+ var RuntimeSession = class {
1316
+ dependencies;
1091
1317
  createMseController;
1092
1318
  createLoader;
1093
1319
  createTransmuxCore;
1320
+ isStarted;
1321
+ isLifecycleContextCurrent;
1322
+ mse;
1323
+ loader;
1324
+ transmuxCore;
1325
+ loaderRunId = 0;
1326
+ loaderClosePromise;
1327
+ outputBytes = 0;
1328
+ appendController;
1329
+ constructor(dependencies) {
1330
+ this.dependencies = dependencies;
1331
+ this.createMseController = dependencies.createMseController ?? (() => new MseController());
1332
+ this.createLoader = dependencies.createLoader ?? ((config) => new HttpFlvLoader(config));
1333
+ this.createTransmuxCore = dependencies.createTransmuxCore ?? ((options) => loadWasmTransmuxCoreHost(options.runtime.wasmUrl));
1334
+ this.isStarted = dependencies.isStarted;
1335
+ this.isLifecycleContextCurrent = dependencies.isLifecycleContextCurrent;
1336
+ this.appendController = new RuntimeAppendController({
1337
+ appendToMse: (segment, context) => this.appendToMse(context, () => this.mse?.appendMediaSegment(segment)),
1338
+ isStarted: dependencies.isStarted,
1339
+ isLifecycleContextCurrent: dependencies.isLifecycleContextCurrent,
1340
+ onAppended: async (segment, context) => {
1341
+ if (!this.isLifecycleContextCurrent(context)) return;
1342
+ this.outputBytes += segment.bytes.byteLength;
1343
+ await dependencies.onMediaAppended(context);
1344
+ },
1345
+ onError: dependencies.onAppendError
1346
+ });
1347
+ }
1348
+ get hasMse() {
1349
+ return this.mse !== void 0;
1350
+ }
1351
+ get loaderStats() {
1352
+ return this.loader?.stats;
1353
+ }
1354
+ get loaderPaused() {
1355
+ return this.loader?.paused ?? false;
1356
+ }
1357
+ get bufferedRanges() {
1358
+ return this.mse?.bufferedRanges ?? [];
1359
+ }
1360
+ get emittedBytes() {
1361
+ return this.outputBytes;
1362
+ }
1363
+ async attach(context) {
1364
+ this.mse ??= this.createMseController();
1365
+ const attachment = await raceLifecycleOperation(this.mse.createMediaSourceHandle(), context.signal);
1366
+ if (attachment.cancelled || !this.isLifecycleContextCurrent(context)) return;
1367
+ return attachment.value;
1368
+ }
1369
+ async createCore(options, context) {
1370
+ const creation = await raceLifecycleOperation(Promise.resolve(this.createTransmuxCore(options)), context.signal, (lateCore) => lateCore?.destroy());
1371
+ if (creation.cancelled || !this.isLifecycleContextCurrent(context)) return;
1372
+ return creation.value;
1373
+ }
1374
+ start(core, context) {
1375
+ this.transmuxCore?.destroy();
1376
+ this.transmuxCore = core;
1377
+ this.outputBytes = 0;
1378
+ this.appendController.start(context);
1379
+ }
1380
+ runLoader(config, context) {
1381
+ const { loader, runId } = this.startLoader(config);
1382
+ this.consumeLoader(loader, runId, context);
1383
+ }
1384
+ async close() {
1385
+ this.dependencies.onLoaderClosing();
1386
+ this.appendController.discard();
1387
+ this.transmuxCore?.destroy();
1388
+ this.transmuxCore = void 0;
1389
+ const loader = this.loader;
1390
+ if (loader === void 0) {
1391
+ await this.loaderClosePromise;
1392
+ return;
1393
+ }
1394
+ this.loader = void 0;
1395
+ this.loaderRunId += 1;
1396
+ await this.closeLoaderInstance(loader);
1397
+ }
1398
+ destroyMse() {
1399
+ this.mse?.destroy();
1400
+ this.mse = void 0;
1401
+ }
1402
+ discardAppend() {
1403
+ this.appendController.discard();
1404
+ }
1405
+ collectMseStats() {
1406
+ return {
1407
+ appendQueueLength: this.mse?.appendQueueLength ?? 0,
1408
+ appendQueueBytes: this.mse?.appendQueueBytes ?? 0,
1409
+ sourceBufferUpdating: this.mse?.sourceBufferUpdating ?? false,
1410
+ sourceBufferCount: this.mse?.sourceBufferCount ?? 0,
1411
+ bufferedRangeCount: this.mse?.bufferedRangeCount ?? 0,
1412
+ bufferedStart: this.mse?.bufferedStart,
1413
+ bufferedEnd: this.mse?.bufferedEnd,
1414
+ bufferedDuration: this.mse?.bufferedDuration
1415
+ };
1416
+ }
1417
+ cleanupBefore(cutoff, force = false) {
1418
+ return this.mse?.cleanupBefore(cutoff, force ? { force: true } : void 0);
1419
+ }
1420
+ pauseLoader() {
1421
+ this.loader?.pause();
1422
+ }
1423
+ resumeLoader() {
1424
+ this.loader?.resume();
1425
+ }
1426
+ startLoader(config) {
1427
+ const loader = this.createLoader(config);
1428
+ const runId = this.loaderRunId + 1;
1429
+ this.loaderRunId = runId;
1430
+ this.loader = loader;
1431
+ return {
1432
+ loader,
1433
+ runId
1434
+ };
1435
+ }
1436
+ pushChunk(bytes) {
1437
+ return this.transmuxCore?.pushChunk(bytes) ?? [];
1438
+ }
1439
+ isCurrentLoader(loader, runId) {
1440
+ return this.loader === loader && this.loaderRunId === runId && this.isStarted();
1441
+ }
1442
+ async closeCurrentLoader(loader, runId) {
1443
+ if (this.loader !== loader || this.loaderRunId !== runId) return;
1444
+ this.dependencies.onLoaderClosing();
1445
+ this.appendController.discard();
1446
+ this.loader = void 0;
1447
+ this.loaderRunId += 1;
1448
+ this.transmuxCore?.destroy();
1449
+ this.transmuxCore = void 0;
1450
+ await this.closeLoaderInstance(loader);
1451
+ }
1452
+ async closeLoaderInstance(loader) {
1453
+ let closing;
1454
+ try {
1455
+ closing = loader.close();
1456
+ } catch (cause) {
1457
+ closing = Promise.reject(cause);
1458
+ }
1459
+ this.loaderClosePromise = closing;
1460
+ try {
1461
+ await closing;
1462
+ } finally {
1463
+ if (this.loaderClosePromise === closing) this.loaderClosePromise = void 0;
1464
+ }
1465
+ }
1466
+ async consumeLoader(loader, runId, context) {
1467
+ try {
1468
+ await loader.open();
1469
+ while (this.isCurrentLoader(loader, runId) && this.isLifecycleContextCurrent(context)) {
1470
+ await this.dependencies.applyLatencyPolicy(context);
1471
+ if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
1472
+ const chunk = await loader.read();
1473
+ if (chunk === null || !this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) break;
1474
+ this.dependencies.onStats(loader.stats);
1475
+ if (!await this.processEvents(this.pushChunk(chunk.bytes), context)) {
1476
+ await this.closeCurrentLoader(loader, runId);
1477
+ return;
1478
+ }
1479
+ await this.dependencies.applyLatencyPolicy(context);
1480
+ if (!this.isCurrentLoader(loader, runId) || !this.isLifecycleContextCurrent(context)) return;
1481
+ this.dependencies.onStats(loader.stats);
1482
+ }
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
+ } catch (cause) {
1488
+ if (!this.isCurrentLoader(loader, runId) || isAbortLikeError(cause)) return;
1489
+ try {
1490
+ await this.closeCurrentLoader(loader, runId);
1491
+ } catch {}
1492
+ if (!this.isLifecycleContextCurrent(context)) return;
1493
+ const code = cause instanceof HttpFlvLoaderError ? cause.code : "RIVMUX_HTTP_LOADER_FAILED";
1494
+ this.dependencies.onFailure("network", code, "HTTP Fetch loader failed.", cause);
1495
+ } finally {
1496
+ if (this.isCurrentLoader(loader, runId)) try {
1497
+ await this.closeCurrentLoader(loader, runId);
1498
+ } catch (cause) {
1499
+ if (this.isLifecycleContextCurrent(context)) this.dependencies.onFailure("network", "RIVMUX_HTTP_LOADER_CLOSE_FAILED", "HTTP Fetch loader failed to close.", cause);
1500
+ }
1501
+ }
1502
+ }
1503
+ async processEvents(events, context) {
1504
+ for (const event of events) {
1505
+ if (!this.isLifecycleContextCurrent(context)) return false;
1506
+ switch (event.type) {
1507
+ case "mediaInfo":
1508
+ this.dependencies.onMessage({
1509
+ type: "media-info",
1510
+ mediaInfo: coreMediaInfoToPlayerMediaInfo(event.data)
1511
+ });
1512
+ break;
1513
+ case "warning":
1514
+ this.dependencies.onMessage({
1515
+ type: "warning",
1516
+ warning: coreWarningToPlayerWarning(event.data)
1517
+ });
1518
+ break;
1519
+ case "fatalError":
1520
+ this.dependencies.onPlayerError(coreErrorToPlayerError(event.data));
1521
+ return false;
1522
+ case "initSegment":
1523
+ if (!await this.appendController.flush(context)) return false;
1524
+ if (!await this.appendToMse(context, () => this.mse?.appendInitSegment(event.data))) return false;
1525
+ if (!this.isLifecycleContextCurrent(context)) return false;
1526
+ this.outputBytes += event.data.bytes.byteLength;
1527
+ await this.dependencies.applyLatencyPolicy(context);
1528
+ break;
1529
+ case "mediaSegment": {
1530
+ const append = this.appendController.push(event.data, context);
1531
+ if (append !== void 0 && !await append) return false;
1532
+ break;
1533
+ }
1534
+ case "probeResult":
1535
+ case "trackConfig":
1536
+ case "sample":
1537
+ case "metadata":
1538
+ case "discontinuity": break;
1539
+ }
1540
+ }
1541
+ return this.appendController.waitForTail();
1542
+ }
1543
+ async appendToMse(context, append) {
1544
+ try {
1545
+ if ((await raceLifecycleOperation(Promise.resolve(append()), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
1546
+ return true;
1547
+ } catch (cause) {
1548
+ if (!this.isLifecycleContextCurrent(context)) return false;
1549
+ if (isQuotaExceededError(cause) && await this.retryAppendAfterQuotaCleanup(context, append)) return true;
1550
+ if (cause instanceof MseUnsupportedMimeError) {
1551
+ this.dependencies.onFailure("unsupported", "RIVMUX_UNSUPPORTED_MSE_CODEC", cause.message, cause);
1552
+ return false;
1553
+ }
1554
+ this.dependencies.onFailure("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", cause);
1555
+ return false;
1556
+ }
1557
+ }
1558
+ async retryAppendAfterQuotaCleanup(context, append) {
1559
+ const cutoff = this.dependencies.quotaCleanupCutoff();
1560
+ if (cutoff === void 0 || cutoff <= 0) return false;
1561
+ try {
1562
+ if ((await raceLifecycleOperation(Promise.resolve(this.cleanupBefore(cutoff, true)), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
1563
+ if ((await raceLifecycleOperation(Promise.resolve(append()), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return false;
1564
+ this.dependencies.onMessage({
1565
+ type: "warning",
1566
+ warning: {
1567
+ code: "RIVMUX_MSE_QUOTA_RETRY",
1568
+ message: "MSE quota was exceeded; old buffered ranges were cleaned before retrying append."
1569
+ }
1570
+ });
1571
+ return true;
1572
+ } catch {
1573
+ return false;
1574
+ }
1575
+ }
1576
+ };
1577
+ function isQuotaExceededError(cause) {
1578
+ return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "QuotaExceededError";
1579
+ }
1580
+ //#endregion
1581
+ //#region src/runtime/index.ts
1582
+ var RuntimeWorker = class {
1583
+ port;
1094
1584
  detectRuntime;
1095
1585
  now;
1096
1586
  state = "idle";
1097
1587
  url;
1098
1588
  options;
1099
- mse;
1100
- loader;
1101
- transmuxCore;
1589
+ session;
1102
1590
  latencyController;
1103
1591
  videoState;
1104
1592
  lastLatencyMetrics = {};
1105
1593
  statsTimer;
1106
1594
  statsTickInFlight = false;
1107
- loaderRunId = 0;
1108
- outputBytes = 0;
1109
1595
  appendQueueMaxLength = 0;
1110
1596
  appendQueueMaxBytes = 0;
1597
+ commandTail = Promise.resolve();
1598
+ lifecycleGeneration = 0;
1599
+ lifecycleAbortController = new AbortController();
1600
+ fatalCleanupPromise = Promise.resolve();
1111
1601
  constructor(port, dependencies = {}) {
1112
1602
  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
1603
  this.detectRuntime = dependencies.detectRuntime ?? detectWorkerRuntime;
1117
1604
  this.now = dependencies.now ?? (() => performance.now());
1605
+ this.session = new RuntimeSession({
1606
+ ...dependencies,
1607
+ isStarted: () => this.state === "started",
1608
+ isLifecycleContextCurrent: (context) => this.isLifecycleContextCurrent(context),
1609
+ onMediaAppended: (context) => this.applyLatencyPolicy(context),
1610
+ onAppendError: (cause, context) => {
1611
+ if (this.isLifecycleContextCurrent(context)) this.fail("mse", "RIVMUX_MSE_APPEND_FAILED", "MSE append failed.", true, cause);
1612
+ },
1613
+ onLoaderClosing: () => this.stopStatsTimer(),
1614
+ onStats: (stats) => this.postStats(stats),
1615
+ onMessage: (message) => this.post(message),
1616
+ onFailure: (kind, code, message, cause) => this.fail(kind, code, message, true, cause),
1617
+ onPlayerError: (error) => this.failWithError(error),
1618
+ applyLatencyPolicy: (context) => this.applyLatencyPolicy(context),
1619
+ quotaCleanupCutoff: () => this.quotaCleanupCutoff()
1620
+ });
1621
+ }
1622
+ handleCommand(command) {
1623
+ if (command.type === "stop" || command.type === "destroy") this.invalidateLifecycle();
1624
+ const context = {
1625
+ generation: this.lifecycleGeneration,
1626
+ signal: this.lifecycleAbortController.signal
1627
+ };
1628
+ const handling = this.commandTail.then(() => this.executeCommand(command, context));
1629
+ this.commandTail = handling.catch(() => void 0);
1630
+ return handling;
1118
1631
  }
1119
- async handleCommand(command) {
1632
+ async executeCommand(command, context) {
1120
1633
  if (this.state === "destroyed") return;
1121
- if (this.state === "fatal-error" && command.type !== "destroy") return;
1634
+ if ((command.type === "attach-media-source" || command.type === "start") && !this.isLifecycleContextCurrent(context)) return;
1635
+ if (this.state === "fatal-error") {
1636
+ if (command.type === "stop") this.post({ type: "stopped" });
1637
+ if (command.type !== "destroy") return;
1638
+ }
1122
1639
  try {
1123
1640
  switch (command.type) {
1124
1641
  case "init":
@@ -1136,10 +1653,10 @@ var RuntimeWorker = class {
1136
1653
  this.post({ type: "ready" });
1137
1654
  return;
1138
1655
  case "attach-media-source":
1139
- await this.attachMediaSource();
1656
+ await this.attachMediaSource(context);
1140
1657
  return;
1141
1658
  case "start":
1142
- await this.start();
1659
+ await this.start(context);
1143
1660
  return;
1144
1661
  case "stop":
1145
1662
  await this.stop();
@@ -1150,8 +1667,7 @@ var RuntimeWorker = class {
1150
1667
  return;
1151
1668
  case "video-state":
1152
1669
  this.videoState = command.state;
1153
- await this.applyLatencyPolicy();
1154
- this.postStats();
1670
+ if (!(await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled && this.isLifecycleContextCurrent(context) && this.state === "started") this.postStats();
1155
1671
  return;
1156
1672
  case "playback-control-result":
1157
1673
  this.latencyController?.recordPlaybackControlResult(command.result);
@@ -1164,57 +1680,55 @@ var RuntimeWorker = class {
1164
1680
  this.fail("runtime", "RIVMUX_WORKER_COMMAND_FAILED", "Worker command failed.", true, cause);
1165
1681
  }
1166
1682
  }
1167
- async attachMediaSource() {
1683
+ async attachMediaSource(context) {
1168
1684
  if (this.state === "idle") {
1169
1685
  this.fail("runtime", "RIVMUX_WORKER_NOT_INITIALIZED", "Worker must be initialized before attach.", true);
1170
1686
  return;
1171
1687
  }
1172
- if (this.mse === void 0) this.mse = this.createMseController();
1173
1688
  try {
1174
- const handle = await this.mse.createMediaSourceHandle();
1689
+ const handle = await this.session.attach(context);
1690
+ if (handle === void 0 || context.generation !== this.lifecycleGeneration) return;
1175
1691
  this.post({
1176
1692
  type: "media-source-handle",
1177
1693
  handle
1178
1694
  }, [handle]);
1179
1695
  this.state = "attached";
1180
1696
  } catch (cause) {
1697
+ if (context.generation !== this.lifecycleGeneration) return;
1181
1698
  this.fail("mse", "RIVMUX_MSE_ATTACH_FAILED", "MSE media source attachment failed.", true, cause);
1182
1699
  }
1183
1700
  }
1184
- async start() {
1701
+ async start(context) {
1185
1702
  const options = this.options;
1186
- if (this.mse === void 0 || options === void 0 || this.state === "idle" || this.state === "ready") {
1703
+ if (!this.session.hasMse || options === void 0 || this.state === "idle" || this.state === "ready") {
1187
1704
  this.fail("runtime", "RIVMUX_WORKER_START_REQUIRES_ATTACH", "Worker start requires an attached MediaSource.", true);
1188
1705
  return;
1189
1706
  }
1190
1707
  if (this.state === "started") return;
1191
- let transmuxCore;
1192
1708
  try {
1193
- const createdCore = await this.createTransmuxCore(options);
1194
- if (createdCore === void 0) {
1709
+ const transmuxCore = await this.session.createCore(options, context);
1710
+ if (context.generation !== this.lifecycleGeneration) return;
1711
+ if (transmuxCore === void 0) {
1195
1712
  this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available.", true);
1196
1713
  return;
1197
1714
  }
1198
- transmuxCore = createdCore;
1715
+ this.session.start(transmuxCore, context);
1199
1716
  } catch (cause) {
1717
+ if (context.generation !== this.lifecycleGeneration) return;
1200
1718
  this.fail("runtime", "RIVMUX_TRANSMUX_CORE_UNAVAILABLE", "Transmux core is not available.", true, cause);
1201
1719
  return;
1202
1720
  }
1203
- this.transmuxCore?.destroy();
1204
- this.transmuxCore = transmuxCore;
1205
1721
  this.state = "started";
1206
- this.outputBytes = 0;
1207
1722
  this.appendQueueMaxLength = 0;
1208
1723
  this.appendQueueMaxBytes = 0;
1724
+ if ((await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled || !this.isLifecycleContextCurrent(context)) return;
1209
1725
  this.startStatsTimer();
1210
- await this.applyLatencyPolicy();
1211
1726
  this.postStats();
1212
- this.startLoader();
1727
+ this.startLoader(context);
1213
1728
  }
1214
1729
  async stop() {
1215
1730
  await this.closeLoader();
1216
- this.mse?.destroy();
1217
- this.mse = void 0;
1731
+ this.session.destroyMse();
1218
1732
  this.latencyController?.reset();
1219
1733
  this.videoState = void 0;
1220
1734
  this.lastLatencyMetrics = {};
@@ -1222,9 +1736,9 @@ var RuntimeWorker = class {
1222
1736
  this.post({ type: "stopped" });
1223
1737
  }
1224
1738
  async destroy() {
1739
+ await this.fatalCleanupPromise;
1225
1740
  await this.closeLoader();
1226
- this.mse?.destroy();
1227
- this.mse = void 0;
1741
+ this.session.destroyMse();
1228
1742
  this.latencyController?.reset();
1229
1743
  this.videoState = void 0;
1230
1744
  this.lastLatencyMetrics = {};
@@ -1232,199 +1746,70 @@ var RuntimeWorker = class {
1232
1746
  this.post({ type: "destroyed" });
1233
1747
  this.port.close();
1234
1748
  }
1235
- startLoader() {
1749
+ startLoader(context) {
1236
1750
  const options = this.options;
1237
1751
  const url = this.url;
1238
1752
  if (options === void 0 || url === void 0) {
1239
1753
  this.fail("runtime", "RIVMUX_WORKER_NOT_INITIALIZED", "Worker must be initialized before loader start.", true);
1240
1754
  return;
1241
1755
  }
1242
- const loader = this.createLoader({
1756
+ this.session.runLoader({
1243
1757
  url,
1244
1758
  network: options.network
1245
- });
1246
- const runId = this.loaderRunId + 1;
1247
- this.loaderRunId = runId;
1248
- this.loader = loader;
1249
- this.runLoader(loader, runId);
1250
- }
1251
- async runLoader(loader, runId) {
1252
- 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);
1265
- }
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);
1270
- return;
1271
- } finally {
1272
- if (this.isCurrentLoader(loader, runId)) await this.closeCurrentLoader(loader, runId);
1273
- }
1759
+ }, context);
1274
1760
  }
1275
1761
  async closeLoader() {
1276
1762
  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();
1293
- }
1294
- isCurrentLoader(loader, runId) {
1295
- return this.loader === loader && this.loaderRunId === runId && this.state === "started";
1763
+ await this.session.close();
1296
1764
  }
1297
1765
  postStats(loaderStats) {
1298
- const metrics = this.lastLatencyMetrics;
1299
1766
  const mseStats = this.collectMseStats();
1300
- const loaderSnapshot = loaderStats ?? this.loader?.stats;
1767
+ const loaderSnapshot = loaderStats ?? this.session.loaderStats;
1301
1768
  this.post({
1302
1769
  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,
1770
+ stats: createPlayerStats({
1771
+ loaderStats: loaderSnapshot,
1772
+ mseStats,
1773
+ latencyMetrics: this.lastLatencyMetrics,
1774
+ outputBytes: this.session.emittedBytes,
1310
1775
  appendQueueMaxLength: this.appendQueueMaxLength,
1311
1776
  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
- }
1777
+ loaderPaused: this.session.loaderPaused,
1778
+ nowMs: this.now()
1779
+ })
1325
1780
  });
1326
1781
  }
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
1782
  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
- }
1783
+ const stats = this.session.collectMseStats();
1784
+ const highWaterMark = updateAppendQueueHighWaterMark({
1785
+ length: this.appendQueueMaxLength,
1786
+ bytes: this.appendQueueMaxBytes
1787
+ }, stats);
1788
+ this.appendQueueMaxLength = highWaterMark.length;
1789
+ this.appendQueueMaxBytes = highWaterMark.bytes;
1790
+ return { ...stats };
1405
1791
  }
1406
1792
  quotaCleanupCutoff() {
1407
1793
  const backwardBuffer = this.options?.latency.backwardBuffer ?? 0;
1408
1794
  const currentTime = this.videoState?.currentTime;
1409
1795
  if (currentTime !== void 0 && Number.isFinite(currentTime)) return Math.max(0, currentTime - backwardBuffer);
1410
- const bufferedEnd = this.mse?.bufferedEnd;
1796
+ const bufferedEnd = this.session.collectMseStats().bufferedEnd;
1411
1797
  return bufferedEnd === void 0 ? void 0 : Math.max(0, bufferedEnd - backwardBuffer);
1412
1798
  }
1413
- async applyLatencyPolicy() {
1799
+ async applyLatencyPolicy(context) {
1414
1800
  const latencyController = this.latencyController;
1415
- const mse = this.mse;
1416
- if (latencyController === void 0 || mse === void 0) return;
1417
- const loader = this.loader;
1801
+ if (latencyController === void 0 || !this.session.hasMse) return;
1418
1802
  const evaluation = latencyController.evaluate({
1419
- ranges: mse.bufferedRanges,
1803
+ ranges: this.session.bufferedRanges,
1420
1804
  videoState: this.videoState,
1421
- loaderPaused: loader?.paused ?? false,
1805
+ loaderPaused: this.session.loaderPaused,
1422
1806
  nowMs: this.now()
1423
1807
  });
1424
1808
  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();
1809
+ if (evaluation.cleanupBefore !== void 0) await this.session.cleanupBefore(evaluation.cleanupBefore);
1810
+ if (context !== void 0 && !this.isLifecycleContextCurrent(context)) return;
1811
+ if (evaluation.loaderCommand === "pause") this.session.pauseLoader();
1812
+ else if (evaluation.loaderCommand === "resume") this.session.resumeLoader();
1428
1813
  if (evaluation.playbackControl !== void 0) this.post({
1429
1814
  type: "playback-control",
1430
1815
  action: evaluation.playbackControl
@@ -1447,15 +1832,25 @@ var RuntimeWorker = class {
1447
1832
  async emitStatsTick() {
1448
1833
  if (this.statsTickInFlight || this.state !== "started") return;
1449
1834
  this.statsTickInFlight = true;
1835
+ const context = this.currentLifecycleContext();
1450
1836
  try {
1451
- await this.applyLatencyPolicy();
1452
- this.postStats();
1837
+ if (!(await raceLifecycleOperation(this.applyLatencyPolicy(context), context.signal)).cancelled && this.isLifecycleContextCurrent(context) && this.state === "started") this.postStats();
1453
1838
  } catch (cause) {
1839
+ if (!this.isLifecycleContextCurrent(context)) return;
1454
1840
  this.fail("mse", "RIVMUX_MSE_LATENCY_POLICY_FAILED", "MSE latency policy failed.", true, cause);
1455
1841
  } finally {
1456
1842
  this.statsTickInFlight = false;
1457
1843
  }
1458
1844
  }
1845
+ currentLifecycleContext() {
1846
+ return {
1847
+ generation: this.lifecycleGeneration,
1848
+ signal: this.lifecycleAbortController.signal
1849
+ };
1850
+ }
1851
+ isLifecycleContextCurrent(context) {
1852
+ return !context.signal.aborted && context.generation === this.lifecycleGeneration;
1853
+ }
1459
1854
  fail(kind, code, message, terminal, cause) {
1460
1855
  const error = cause === void 0 ? {
1461
1856
  kind,
@@ -1472,6 +1867,7 @@ var RuntimeWorker = class {
1472
1867
  this.failWithError(error);
1473
1868
  }
1474
1869
  failWithError(error) {
1870
+ if (this.state === "destroyed") return;
1475
1871
  if (error.terminal) this.enterFatalErrorState();
1476
1872
  this.post({
1477
1873
  type: "error",
@@ -1479,11 +1875,12 @@ var RuntimeWorker = class {
1479
1875
  });
1480
1876
  }
1481
1877
  enterFatalErrorState() {
1482
- if (this.state === "fatal-error") return;
1878
+ if (this.state === "fatal-error" || this.state === "destroyed") return;
1483
1879
  this.state = "fatal-error";
1484
- this.closeLoader();
1485
- this.mse?.destroy();
1486
- this.mse = void 0;
1880
+ this.invalidateLifecycle();
1881
+ this.session.discardAppend();
1882
+ this.fatalCleanupPromise = this.closeLoader().catch(() => void 0);
1883
+ this.session.destroyMse();
1487
1884
  this.latencyController?.reset();
1488
1885
  this.videoState = void 0;
1489
1886
  this.lastLatencyMetrics = {};
@@ -1491,15 +1888,12 @@ var RuntimeWorker = class {
1491
1888
  post(message, transfer) {
1492
1889
  this.port.postMessage(message, transfer);
1493
1890
  }
1891
+ invalidateLifecycle() {
1892
+ this.lifecycleGeneration += 1;
1893
+ this.lifecycleAbortController.abort();
1894
+ this.lifecycleAbortController = new AbortController();
1895
+ }
1494
1896
  };
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);
1502
- }
1503
1897
  function serializeCause(cause) {
1504
1898
  if (cause instanceof Error) return {
1505
1899
  name: cause.name,
@@ -1507,12 +1901,6 @@ function serializeCause(cause) {
1507
1901
  };
1508
1902
  return cause;
1509
1903
  }
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;
1515
- }
1516
1904
  function detectWorkerRuntime() {
1517
1905
  if (typeof fetch !== "function") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_FETCH", "Fetch is not available in this worker runtime.");
1518
1906
  if (typeof ReadableStream === "undefined") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_READABLE_STREAM", "ReadableStream is not available in this worker runtime.");
@@ -1520,7 +1908,6 @@ function detectWorkerRuntime() {
1520
1908
  if (typeof MediaSource === "undefined") return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_MSE", "MediaSource is not available in this worker runtime.");
1521
1909
  if (MediaSource.canConstructInDedicatedWorker !== true) return createUnsupportedRuntimeError("RIVMUX_UNSUPPORTED_WORKER_MSE", "MediaSource cannot be constructed in this worker runtime.");
1522
1910
  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
1911
  }
1525
1912
  function createUnsupportedRuntimeError(code, message) {
1526
1913
  return {
@@ -1536,38 +1923,6 @@ function createLatencyController(options) {
1536
1923
  playback: options.playback
1537
1924
  });
1538
1925
  }
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
1926
  //#endregion
1572
1927
  //#region src/worker-entry.ts
1573
1928
  function startRuntimeWorker(scope) {