avbridge 1.0.0 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,138 @@ All notable changes to **avbridge** are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [2.0.0]
8
+
9
+ ### Breaking changes
10
+
11
+ - **`createPlayer({ forceStrategy })` renamed to `{ initialStrategy }`.**
12
+ The old name implied a hard force, but the player has always walked the
13
+ fallback chain on failure regardless. The new name matches the actual
14
+ semantics: the named strategy is the *initial* pick, and escalation still
15
+ applies if it fails. **Migration:** rename the option key. Behavior is
16
+ unchanged for consumers who passed a strategy that successfully started.
17
+
18
+ - **`discoverSidecar()` renamed to `discoverSidecars()`.** The function
19
+ always returned an array; the singular name was misleading. **Migration:**
20
+ rename the import. No semantic change.
21
+
22
+ - **`<avbridge-player>` was renamed to `<avbridge-video>`.** The element is
23
+ semantically an `HTMLMediaElement`-compatible primitive — it has no UI,
24
+ no controls, and is intended as a drop-in replacement for `<video>`. The
25
+ old name oversold what it does. The new name matches the contract.
26
+ - The class export is now `AvbridgeVideoElement` (was `AvbridgePlayerElement`).
27
+ - The source file is `src/element/avbridge-video.ts`.
28
+ - The subpath import is unchanged: `import "avbridge/element"` still
29
+ registers the element. It now registers `<avbridge-video>` instead of
30
+ `<avbridge-player>`.
31
+ - **Migration**: rename every `<avbridge-player>` tag, every
32
+ `AvbridgePlayerElement` reference, and any CSS selectors targeting the
33
+ old tag. There is no compatibility shim — `<avbridge-player>` is **not**
34
+ registered in 2.0 because the name is reserved for the future
35
+ controls-bearing element.
36
+
37
+ ### Reserved
38
+
39
+ - **`<avbridge-player>` is reserved** for a future controls-bearing element
40
+ that ships built-in player UI (seek bar, play/pause, subtitle/audio menus,
41
+ drag-and-drop, etc.). It does not exist yet. Importing `avbridge/element`
42
+ registers only `<avbridge-video>`.
43
+
44
+ ### Fixed
45
+
46
+ - **Unknown video/audio codecs no longer silently relabel as `h264`/`aac`.**
47
+ `mediabunnyVideoToAvbridge()` and `mediabunnyAudioToAvbridge()` used to
48
+ default unknown inputs to the most common codec name, which sent
49
+ unsupported media down the native/remux path and produced opaque "playback
50
+ failed" errors. They now preserve the original codec string (or return
51
+ `"unknown"` for `null`/`undefined`), and the classifier routes anything
52
+ outside `NATIVE_VIDEO_CODECS` / `NATIVE_AUDIO_CODECS` to fallback as
53
+ intended. Reported in CODE_REVIEW finding #1.
54
+
55
+ - **Strategy escalation now walks the entire fallback chain.** Previously,
56
+ if an intermediate fallback step failed to start, `doEscalate()` emitted
57
+ an error and gave up — even when later viable strategies remained in the
58
+ chain. This was inconsistent with the initial bootstrap path
59
+ (`startSession`), which already recurses. Escalation now loops until a
60
+ strategy starts or the chain is exhausted, and the final error message
61
+ lists every attempted strategy with its individual failure reason.
62
+ Reported in CODE_REVIEW finding #4.
63
+
64
+ - **`initialStrategy` now reports the correct `strategyClass` in
65
+ diagnostics.** The old `forceStrategy` path hard-coded
66
+ `class: "NATIVE"` regardless of the picked strategy, so any downstream
67
+ logic that trusted `strategyClass` got the wrong answer for forced
68
+ remux/hybrid/fallback runs. The class is now derived from the actual
69
+ picked strategy. Reported in CODE_REVIEW finding #2.
70
+
71
+ - **`<avbridge-video>`'s `preferredStrategy` is now wired through to
72
+ `createPlayer({ initialStrategy })`.** It was previously inert — settable
73
+ but ignored at bootstrap. Reported in CODE_REVIEW finding #7.
74
+
75
+ - **Subtitle attachment is awaited during bootstrap and per-track failures
76
+ are caught.** `attachSubtitleTracks()` was previously called without
77
+ `await`, so fetch/parse errors became unhandled rejections and `ready`
78
+ could fire before subtitle tracks existed. The call is now awaited, and
79
+ individual track failures are caught via an `onError` callback so a
80
+ single bad sidecar doesn't break bootstrap. The player logs them via
81
+ `console.warn`; promoting that to a typed `subtitleerror` event on the
82
+ player is a follow-up. Subtitles are not load-bearing for playback.
83
+ Reported in CODE_REVIEW finding #3.
84
+
85
+ - **Subtitle blob URLs are now revoked at player teardown.** Sidecar
86
+ discovery and SRT→VTT conversion both created blob URLs that were never
87
+ revoked, leaking memory across repeated source swaps in long-lived SPAs.
88
+ A new `SubtitleResourceBag` owns every URL the player creates and
89
+ releases them in `destroy()`. Reported in CODE_REVIEW finding #5.
90
+
91
+ - **Diagnostics no longer claim `rangeSupported: true` for URL inputs by
92
+ default.** The old code inferred Range support from the input type
93
+ (`typeof src === "string" → rangeSupported: true`), but the native and
94
+ remux URL paths rely on the browser's or mediabunny's own Range handling
95
+ and don't fail-fast on a non-supporting server, so the claim could be a
96
+ lie. The field is now `undefined` until a strategy actively confirms it
97
+ via the new `Diagnostics.recordTransport()` hook. Reported in
98
+ CODE_REVIEW finding #6.
99
+
100
+ ## [1.1.0]
101
+
102
+ ### Added
103
+
104
+ - **`<avbridge-player>` is now a true `<video>` drop-in.** The element gained
105
+ the missing slice of the `HTMLMediaElement` surface so existing code that
106
+ reaches for a `<video>` can swap to `<avbridge-player>` with no behavioural
107
+ changes:
108
+ - **Properties**: `poster`, `volume`, `playbackRate`, `videoWidth`,
109
+ `videoHeight`, `played`, `seekable`, `crossOrigin`, `disableRemotePlayback`.
110
+ - **Method**: `canPlayType(mimeType)` — passes through to the underlying
111
+ `<video>`. Note that this answers about the *browser's* native support,
112
+ not avbridge's full capabilities.
113
+ - **Attributes** (reflected to the inner `<video>`): `poster`, `playsinline`,
114
+ `crossorigin`, `disableremoteplayback`.
115
+ - **Event forwarding**: 17 standard `HTMLMediaElement` events are forwarded
116
+ from the inner `<video>` to the wrapper element — `loadstart`,
117
+ `loadedmetadata`, `loadeddata`, `canplay`, `canplaythrough`, `play`,
118
+ `playing`, `pause`, `seeking`, `seeked`, `volumechange`, `ratechange`,
119
+ `durationchange`, `waiting`, `stalled`, `emptied`, `resize`. Consumers can
120
+ `el.addEventListener("loadedmetadata", …)` exactly like a real `<video>`.
121
+ - **`<track>` children**: light-DOM `<track>` elements declared as children
122
+ of `<avbridge-player>` are now mirrored into the shadow `<video>` and kept
123
+ in sync via a `MutationObserver`. This works for static HTML markup as
124
+ well as dynamic insertion / removal.
125
+ - **`videoElement` getter**: escape hatch returning the underlying shadow
126
+ `<video>` for native APIs the wrapper doesn't expose
127
+ (`requestPictureInPicture`, browser-native `audioTracks`, `captureStream`,
128
+ library integrations needing a real `HTMLVideoElement`). Caveat: when the
129
+ active strategy is `"fallback"` or `"hybrid"`, frames render to a canvas
130
+ overlay rather than into this `<video>`, so APIs that depend on the actual
131
+ pixels won't show the playing content in those modes.
132
+
133
+ ### Changed
134
+
135
+ - The element's `observedAttributes` list grew to include `poster`,
136
+ `playsinline`, `crossorigin`, and `disableremoteplayback`.
137
+
138
+ ## [1.0.0]
8
139
 
9
140
  ### Added
10
141
 
package/README.md CHANGED
@@ -348,7 +348,7 @@ core (native + remux for modern containers) doesn't need any of this.
348
348
  - libav.js **threading is disabled** due to bugs in v6.8.8 — decode runs single-threaded with SIMD acceleration.
349
349
  - `transcode()` v1 only accepts mediabunny-readable inputs (MP4/MKV/WebM/OGG/MOV/MP3/FLAC/WAV). AVI/ASF/FLV transcoding is planned for v1.1.
350
350
  - `transcode()` uses **WebCodecs encoders only** — codec availability depends on the browser. AV1 encoding is not yet universal.
351
- - For the **hybrid and fallback strategies**, `<avbridge-player>.buffered` returns an empty `TimeRanges` because the canvas-based renderers don't track buffered ranges yet. Native and remux strategies expose the full `<video>.buffered` set as expected.
351
+ - For the **hybrid and fallback strategies**, `<avbridge-video>.buffered` returns an empty `TimeRanges` because the canvas-based renderers don't track buffered ranges yet. Native and remux strategies expose the full `<video>.buffered` set as expected.
352
352
 
353
353
  ## Demos
354
354
 
@@ -90,7 +90,7 @@ function mediabunnyVideoToAvbridge(c) {
90
90
  case "av1":
91
91
  return "av1";
92
92
  default:
93
- return "h264";
93
+ return c ? c : "unknown";
94
94
  }
95
95
  }
96
96
  function avbridgeVideoToMediabunny(c) {
@@ -126,7 +126,7 @@ function mediabunnyAudioToAvbridge(c) {
126
126
  case "eac3":
127
127
  return "eac3";
128
128
  default:
129
- return c ?? "aac";
129
+ return c ? c : "unknown";
130
130
  }
131
131
  }
132
132
  function avbridgeAudioToMediabunny(c) {
@@ -533,19 +533,38 @@ var Diagnostics = class {
533
533
  if (typeof src === "string" || src instanceof URL) {
534
534
  this.sourceType = "url";
535
535
  this.transport = "http-range";
536
- this.rangeSupported = true;
536
+ this.rangeSupported = void 0;
537
537
  } else {
538
538
  this.sourceType = "blob";
539
539
  this.transport = "memory";
540
+ this.rangeSupported = false;
540
541
  }
541
542
  }
543
+ /**
544
+ * Called by a strategy once it has a confirmed answer about how the
545
+ * source bytes are actually flowing (e.g. after the libav HTTP block
546
+ * reader's initial Range probe succeeded). Lets diagnostics report the
547
+ * truth instead of an input-type heuristic.
548
+ */
549
+ recordTransport(transport, rangeSupported) {
550
+ this.transport = transport;
551
+ this.rangeSupported = rangeSupported;
552
+ }
542
553
  recordClassification(c) {
543
554
  this.strategy = c.strategy;
544
555
  this.strategyClass = c.class;
545
556
  this.reason = c.reason;
546
557
  }
547
558
  recordRuntime(stats) {
548
- this.runtime = { ...this.runtime, ...stats };
559
+ const {
560
+ _transport,
561
+ _rangeSupported,
562
+ ...rest
563
+ } = stats;
564
+ if (_transport != null && typeof _rangeSupported === "boolean") {
565
+ this.recordTransport(_transport, _rangeSupported);
566
+ }
567
+ this.runtime = { ...this.runtime, ...rest };
549
568
  }
550
569
  recordStrategySwitch(strategy, reason) {
551
570
  this.strategy = strategy;
@@ -1707,6 +1726,9 @@ async function startHybridDecoder(opts) {
1707
1726
  videoChunksFed,
1708
1727
  audioFramesDecoded,
1709
1728
  videoDecodeQueueSize: videoDecoder?.decodeQueueSize ?? 0,
1729
+ // Confirmed transport info — see fallback decoder for the pattern.
1730
+ _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
1731
+ _rangeSupported: inputHandle.transport === "http-range",
1710
1732
  ...opts.renderer.stats(),
1711
1733
  ...opts.audio.stats()
1712
1734
  };
@@ -2248,6 +2270,12 @@ async function startDecoder(opts) {
2248
2270
  packetsRead,
2249
2271
  videoFramesDecoded,
2250
2272
  audioFramesDecoded,
2273
+ // Confirmed transport info: once prepareLibavInput returns
2274
+ // successfully, we *know* whether the source is http-range (probe
2275
+ // succeeded and returned 206) or in-memory blob. Diagnostics hoists
2276
+ // these `_`-prefixed keys to the typed fields.
2277
+ _transport: inputHandle.transport === "http-range" ? "http-range" : "memory",
2278
+ _rangeSupported: inputHandle.transport === "http-range",
2251
2279
  ...opts.renderer.stats(),
2252
2280
  ...opts.audio.stats()
2253
2281
  };
@@ -2529,7 +2557,7 @@ function isVtt(text) {
2529
2557
  }
2530
2558
 
2531
2559
  // src/subtitles/index.ts
2532
- async function discoverSidecar(file, directory) {
2560
+ async function discoverSidecars(file, directory) {
2533
2561
  const baseName = file.name.replace(/\.[^.]+$/, "");
2534
2562
  const found = [];
2535
2563
  for await (const [name, handle] of directory) {
@@ -2551,33 +2579,56 @@ async function discoverSidecar(file, directory) {
2551
2579
  }
2552
2580
  return found;
2553
2581
  }
2554
- async function attachSubtitleTracks(video, tracks) {
2582
+ var SubtitleResourceBag = class {
2583
+ urls = /* @__PURE__ */ new Set();
2584
+ /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */
2585
+ track(url) {
2586
+ this.urls.add(url);
2587
+ }
2588
+ /** Convenience: create a blob URL and track it in one call. */
2589
+ createObjectURL(blob) {
2590
+ const url = URL.createObjectURL(blob);
2591
+ this.urls.add(url);
2592
+ return url;
2593
+ }
2594
+ /** Revoke every tracked URL. Idempotent — safe to call multiple times. */
2595
+ revokeAll() {
2596
+ for (const u of this.urls) URL.revokeObjectURL(u);
2597
+ this.urls.clear();
2598
+ }
2599
+ };
2600
+ async function attachSubtitleTracks(video, tracks, bag, onError) {
2555
2601
  for (const t of Array.from(video.querySelectorAll("track[data-avbridge]"))) {
2556
2602
  t.remove();
2557
2603
  }
2558
2604
  for (const t of tracks) {
2559
2605
  if (!t.sidecarUrl) continue;
2560
- let url = t.sidecarUrl;
2561
- if (t.format === "srt") {
2562
- const res = await fetch(t.sidecarUrl);
2563
- const text = await res.text();
2564
- const vtt = srtToVtt(text);
2565
- const blob = new Blob([vtt], { type: "text/vtt" });
2566
- url = URL.createObjectURL(blob);
2567
- } else if (t.format === "vtt") {
2568
- const res = await fetch(t.sidecarUrl);
2569
- const text = await res.text();
2570
- if (!isVtt(text)) {
2571
- console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
2572
- }
2573
- }
2574
- const track = document.createElement("track");
2575
- track.kind = "subtitles";
2576
- track.src = url;
2577
- track.srclang = t.language ?? "und";
2578
- track.label = t.language ?? `Subtitle ${t.id}`;
2579
- track.dataset.avbridge = "true";
2580
- video.appendChild(track);
2606
+ try {
2607
+ let url = t.sidecarUrl;
2608
+ if (t.format === "srt") {
2609
+ const res = await fetch(t.sidecarUrl);
2610
+ const text = await res.text();
2611
+ const vtt = srtToVtt(text);
2612
+ const blob = new Blob([vtt], { type: "text/vtt" });
2613
+ url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);
2614
+ } else if (t.format === "vtt") {
2615
+ const res = await fetch(t.sidecarUrl);
2616
+ const text = await res.text();
2617
+ if (!isVtt(text)) {
2618
+ console.warn("[avbridge] subtitle missing WEBVTT header:", t.sidecarUrl);
2619
+ }
2620
+ }
2621
+ const trackEl = document.createElement("track");
2622
+ trackEl.kind = "subtitles";
2623
+ trackEl.src = url;
2624
+ trackEl.srclang = t.language ?? "und";
2625
+ trackEl.label = t.language ?? `Subtitle ${t.id}`;
2626
+ trackEl.dataset.avbridge = "true";
2627
+ video.appendChild(trackEl);
2628
+ } catch (err) {
2629
+ const e = err instanceof Error ? err : new Error(String(err));
2630
+ onError?.(e, t);
2631
+ }
2581
2632
  }
2582
2633
  }
2583
2634
 
@@ -2606,6 +2657,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
2606
2657
  errorListener = null;
2607
2658
  // Serializes escalation / setStrategy calls
2608
2659
  switchingPromise = Promise.resolve();
2660
+ // Owns blob URLs created during sidecar discovery + SRT->VTT conversion.
2661
+ // Revoked at destroy() so repeated source swaps don't leak.
2662
+ subtitleResources = new SubtitleResourceBag();
2609
2663
  static async create(options) {
2610
2664
  const registry = new PluginRegistry();
2611
2665
  registerBuiltins(registry);
@@ -2641,8 +2695,9 @@ var UnifiedPlayer = class _UnifiedPlayer {
2641
2695
  }
2642
2696
  }
2643
2697
  if (this.options.directory && this.options.source instanceof File) {
2644
- const found = await discoverSidecar(this.options.source, this.options.directory);
2698
+ const found = await discoverSidecars(this.options.source, this.options.directory);
2645
2699
  for (const s of found) {
2700
+ this.subtitleResources.track(s.url);
2646
2701
  ctx.subtitleTracks.push({
2647
2702
  id: ctx.subtitleTracks.length,
2648
2703
  format: s.format,
@@ -2651,11 +2706,7 @@ var UnifiedPlayer = class _UnifiedPlayer {
2651
2706
  });
2652
2707
  }
2653
2708
  }
2654
- const decision = this.options.forceStrategy ? {
2655
- class: "NATIVE",
2656
- strategy: this.options.forceStrategy,
2657
- reason: `forced via options.forceStrategy=${this.options.forceStrategy}`
2658
- } : classifyContext(ctx);
2709
+ const decision = this.options.initialStrategy ? buildInitialDecision(this.options.initialStrategy, ctx) : classifyContext(ctx);
2659
2710
  this.classification = decision;
2660
2711
  this.diag.recordClassification(decision);
2661
2712
  this.emitter.emitSticky("strategy", {
@@ -2664,7 +2715,14 @@ var UnifiedPlayer = class _UnifiedPlayer {
2664
2715
  });
2665
2716
  await this.startSession(decision.strategy, decision.reason);
2666
2717
  if (this.session.strategy !== "fallback" && this.session.strategy !== "hybrid") {
2667
- attachSubtitleTracks(this.options.target, ctx.subtitleTracks);
2718
+ await attachSubtitleTracks(
2719
+ this.options.target,
2720
+ ctx.subtitleTracks,
2721
+ this.subtitleResources,
2722
+ (err, track) => {
2723
+ console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
2724
+ }
2725
+ );
2668
2726
  }
2669
2727
  this.emitter.emitSticky("tracks", {
2670
2728
  video: ctx.videoTracks,
@@ -2739,15 +2797,6 @@ var UnifiedPlayer = class _UnifiedPlayer {
2739
2797
  const currentTime = this.session?.getCurrentTime() ?? 0;
2740
2798
  const wasPlaying = this.session ? !this.options.target.paused : false;
2741
2799
  const fromStrategy = this.session?.strategy ?? "native";
2742
- const nextStrategy = chain.shift();
2743
- console.warn(`[avbridge] escalating from ${fromStrategy} to ${nextStrategy}: ${reason}`);
2744
- this.emitter.emit("strategychange", {
2745
- from: fromStrategy,
2746
- to: nextStrategy,
2747
- reason,
2748
- currentTime
2749
- });
2750
- this.diag.recordStrategySwitch(nextStrategy, reason);
2751
2800
  this.clearSupervisor();
2752
2801
  if (this.session) {
2753
2802
  try {
@@ -2756,31 +2805,49 @@ var UnifiedPlayer = class _UnifiedPlayer {
2756
2805
  }
2757
2806
  this.session = null;
2758
2807
  }
2759
- const plugin = this.registry.findFor(this.mediaContext, nextStrategy);
2760
- if (!plugin) {
2761
- this.emitter.emit("error", new Error(`no plugin for fallback strategy "${nextStrategy}"`));
2762
- return;
2763
- }
2764
- try {
2765
- this.session = await plugin.execute(this.mediaContext, this.options.target);
2766
- } catch (err) {
2767
- this.emitter.emit("error", err instanceof Error ? err : new Error(String(err)));
2808
+ const errors = [];
2809
+ while (chain.length > 0) {
2810
+ const nextStrategy = chain.shift();
2811
+ console.warn(`[avbridge] escalating from ${fromStrategy} to ${nextStrategy}: ${reason}`);
2812
+ this.emitter.emit("strategychange", {
2813
+ from: fromStrategy,
2814
+ to: nextStrategy,
2815
+ reason,
2816
+ currentTime
2817
+ });
2818
+ this.diag.recordStrategySwitch(nextStrategy, reason);
2819
+ const plugin = this.registry.findFor(this.mediaContext, nextStrategy);
2820
+ if (!plugin) {
2821
+ errors.push(`${nextStrategy}: no plugin available`);
2822
+ continue;
2823
+ }
2824
+ try {
2825
+ this.session = await plugin.execute(this.mediaContext, this.options.target);
2826
+ } catch (err) {
2827
+ const msg = err instanceof Error ? err.message : String(err);
2828
+ errors.push(`${nextStrategy}: ${msg}`);
2829
+ console.warn(`[avbridge] ${nextStrategy} failed during escalation, trying next: ${msg}`);
2830
+ continue;
2831
+ }
2832
+ this.emitter.emitSticky("strategy", {
2833
+ strategy: nextStrategy,
2834
+ reason: `escalated: ${reason}`
2835
+ });
2836
+ this.session.onFatalError?.((fatalReason) => {
2837
+ void this.escalate(fatalReason);
2838
+ });
2839
+ this.attachSupervisor();
2840
+ try {
2841
+ await this.session.seek(currentTime);
2842
+ if (wasPlaying) await this.session.play();
2843
+ } catch (err) {
2844
+ console.warn("[avbridge] failed to restore position after escalation:", err);
2845
+ }
2768
2846
  return;
2769
2847
  }
2770
- this.emitter.emitSticky("strategy", {
2771
- strategy: nextStrategy,
2772
- reason: `escalated: ${reason}`
2773
- });
2774
- this.session.onFatalError?.((fatalReason) => {
2775
- void this.escalate(fatalReason);
2776
- });
2777
- this.attachSupervisor();
2778
- try {
2779
- await this.session.seek(currentTime);
2780
- if (wasPlaying) await this.session.play();
2781
- } catch (err) {
2782
- console.warn("[avbridge] failed to restore position after escalation:", err);
2783
- }
2848
+ this.emitter.emit("error", new Error(
2849
+ `all fallback strategies failed: ${errors.join("; ")}`
2850
+ ));
2784
2851
  }
2785
2852
  // ── Stall supervision ─────────────────────────────────────────────────
2786
2853
  attachSupervisor() {
@@ -2945,13 +3012,49 @@ var UnifiedPlayer = class _UnifiedPlayer {
2945
3012
  await this.session.destroy();
2946
3013
  this.session = null;
2947
3014
  }
3015
+ this.subtitleResources.revokeAll();
2948
3016
  this.emitter.removeAll();
2949
3017
  }
2950
3018
  };
2951
3019
  async function createPlayer(options) {
2952
3020
  return UnifiedPlayer.create(options);
2953
3021
  }
3022
+ function buildInitialDecision(initial, ctx) {
3023
+ const natural = classifyContext(ctx);
3024
+ const cls = strategyToClass(initial, natural);
3025
+ return {
3026
+ class: cls,
3027
+ strategy: initial,
3028
+ reason: `initial strategy "${initial}" requested via options.initialStrategy`,
3029
+ fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial)
3030
+ };
3031
+ }
3032
+ function strategyToClass(strategy, natural) {
3033
+ if (natural.strategy === strategy) return natural.class;
3034
+ switch (strategy) {
3035
+ case "native":
3036
+ return "NATIVE";
3037
+ case "remux":
3038
+ return "REMUX_CANDIDATE";
3039
+ case "hybrid":
3040
+ return "HYBRID_CANDIDATE";
3041
+ case "fallback":
3042
+ return "FALLBACK_REQUIRED";
3043
+ }
3044
+ }
3045
+ function defaultFallbackChain(strategy) {
3046
+ switch (strategy) {
3047
+ case "native":
3048
+ return ["remux", "hybrid", "fallback"];
3049
+ case "remux":
3050
+ return ["hybrid", "fallback"];
3051
+ case "hybrid":
3052
+ return ["fallback"];
3053
+ case "fallback":
3054
+ return [];
3055
+ }
3056
+ }
2954
3057
 
2955
3058
  export { UnifiedPlayer, avbridgeAudioToMediabunny, avbridgeVideoToMediabunny, buildMediabunnySourceFromInput, classifyContext, createPlayer, probe, srtToVtt };
2956
- //# sourceMappingURL=chunk-FKM7QBZU.js.map
2957
- //# sourceMappingURL=chunk-FKM7QBZU.js.map
3059
+ //# sourceMappingURL=chunk-HZF5JDOO.js.map
3060
+ //# sourceMappingURL=chunk-HZF5JDOO.js.map