avbridge 1.1.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 +93 -0
- package/README.md +1 -1
- package/dist/{chunk-FKM7QBZU.js → chunk-HZF5JDOO.js} +171 -68
- package/dist/chunk-HZF5JDOO.js.map +1 -0
- package/dist/{chunk-F7YS2XOA.cjs → chunk-TTV56KDB.cjs} +171 -68
- package/dist/chunk-TTV56KDB.cjs.map +1 -0
- package/dist/element.cjs +20 -9
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +23 -16
- package/dist/element.d.ts +23 -16
- package/dist/element.js +19 -8
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{player-BBwbCkdL.d.cts → player-BdtUG4rh.d.cts} +10 -2
- package/dist/{player-BBwbCkdL.d.ts → player-BdtUG4rh.d.ts} +10 -2
- package/package.json +1 -1
- package/src/diagnostics.ts +42 -4
- package/src/element/{avbridge-player.ts → avbridge-video.ts} +27 -14
- package/src/element.ts +9 -5
- package/src/player.ts +131 -50
- package/src/probe/mediabunny.ts +8 -2
- package/src/strategies/fallback/decoder.ts +6 -0
- package/src/strategies/hybrid/decoder.ts +3 -0
- package/src/subtitles/index.ts +69 -22
- package/src/types.ts +9 -2
- package/dist/chunk-F7YS2XOA.cjs.map +0 -1
- package/dist/chunk-FKM7QBZU.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,99 @@ 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
|
+
## [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
|
+
|
|
7
100
|
## [1.1.0]
|
|
8
101
|
|
|
9
102
|
### Added
|
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-
|
|
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 "
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
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.
|
|
2771
|
-
|
|
2772
|
-
|
|
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-
|
|
2957
|
-
//# sourceMappingURL=chunk-
|
|
3059
|
+
//# sourceMappingURL=chunk-HZF5JDOO.js.map
|
|
3060
|
+
//# sourceMappingURL=chunk-HZF5JDOO.js.map
|