avbridge 2.2.1 → 2.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.
- package/CHANGELOG.md +153 -1
- package/NOTICE.md +2 -2
- package/README.md +2 -3
- package/THIRD_PARTY_LICENSES.md +2 -2
- package/dist/avi-2JPBSHGA.js +183 -0
- package/dist/avi-2JPBSHGA.js.map +1 -0
- package/dist/avi-F6WZJK5T.cjs +185 -0
- package/dist/avi-F6WZJK5T.cjs.map +1 -0
- package/dist/{avi-GCGM7OJI.js → avi-NJXAXUXK.js} +9 -3
- package/dist/avi-NJXAXUXK.js.map +1 -0
- package/dist/{avi-6SJLWIWW.cjs → avi-W6L3BTWU.cjs} +10 -4
- package/dist/avi-W6L3BTWU.cjs.map +1 -0
- package/dist/chunk-2IJ66NTD.cjs +212 -0
- package/dist/chunk-2IJ66NTD.cjs.map +1 -0
- package/dist/{chunk-ILKDNBSE.js → chunk-2XW2O3YI.cjs} +55 -10
- package/dist/chunk-2XW2O3YI.cjs.map +1 -0
- package/dist/chunk-5KVLE6YI.js +167 -0
- package/dist/chunk-5KVLE6YI.js.map +1 -0
- package/dist/chunk-5YAWWKA3.js +18 -0
- package/dist/chunk-5YAWWKA3.js.map +1 -0
- package/dist/chunk-CPJLFFCC.js +189 -0
- package/dist/chunk-CPJLFFCC.js.map +1 -0
- package/dist/chunk-CPZ7PXAM.cjs +240 -0
- package/dist/chunk-CPZ7PXAM.cjs.map +1 -0
- package/dist/{chunk-WD2ZNQA7.js → chunk-DCSOQH2N.js} +7 -4
- package/dist/chunk-DCSOQH2N.js.map +1 -0
- package/dist/{chunk-HZLQNKFN.cjs → chunk-E76AMWI4.js} +40 -15
- package/dist/chunk-E76AMWI4.js.map +1 -0
- package/dist/chunk-F3LQJKXK.cjs +20 -0
- package/dist/chunk-F3LQJKXK.cjs.map +1 -0
- package/dist/chunk-IAYKFGFG.js +200 -0
- package/dist/chunk-IAYKFGFG.js.map +1 -0
- package/dist/{chunk-DMWARSEF.js → chunk-KY2GPCT7.js} +788 -697
- package/dist/chunk-KY2GPCT7.js.map +1 -0
- package/dist/chunk-LUFA47FP.js +19 -0
- package/dist/chunk-LUFA47FP.js.map +1 -0
- package/dist/chunk-NNVOHKXJ.cjs +204 -0
- package/dist/chunk-NNVOHKXJ.cjs.map +1 -0
- package/dist/chunk-Q2VUO52Z.cjs +374 -0
- package/dist/chunk-Q2VUO52Z.cjs.map +1 -0
- package/dist/chunk-QDJLQR53.cjs +22 -0
- package/dist/chunk-QDJLQR53.cjs.map +1 -0
- package/dist/chunk-S4WAZC2T.cjs +173 -0
- package/dist/chunk-S4WAZC2T.cjs.map +1 -0
- package/dist/chunk-SMH6IOP2.js +368 -0
- package/dist/chunk-SMH6IOP2.js.map +1 -0
- package/dist/chunk-SR3MPV4D.js +237 -0
- package/dist/chunk-SR3MPV4D.js.map +1 -0
- package/dist/{chunk-UF2N5L63.cjs → chunk-TBW26OPP.cjs} +800 -710
- package/dist/chunk-TBW26OPP.cjs.map +1 -0
- package/dist/chunk-X2K3GIWE.js +235 -0
- package/dist/chunk-X2K3GIWE.js.map +1 -0
- package/dist/{chunk-L4NPOJ36.cjs → chunk-Z33SBWL5.cjs} +7 -4
- package/dist/chunk-Z33SBWL5.cjs.map +1 -0
- package/dist/chunk-ZCUXHW55.cjs +242 -0
- package/dist/chunk-ZCUXHW55.cjs.map +1 -0
- package/dist/element-browser.js +1282 -503
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +59 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +39 -1
- package/dist/element.d.ts +39 -1
- package/dist/element.js +58 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +605 -327
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +528 -319
- package/dist/index.js.map +1 -1
- package/dist/libav-demux-H2GS46GH.cjs +27 -0
- package/dist/{libav-http-reader-NQJVY273.js.map → libav-demux-H2GS46GH.cjs.map} +1 -1
- package/dist/libav-demux-OWZ4T2YW.js +6 -0
- package/dist/{libav-http-reader-FPYDBMYK.cjs.map → libav-demux-OWZ4T2YW.js.map} +1 -1
- package/dist/libav-http-reader-AZLE7YFS.cjs +16 -0
- package/dist/libav-http-reader-AZLE7YFS.cjs.map +1 -0
- package/dist/libav-http-reader-WXG3Z7AI.js +3 -0
- package/dist/libav-http-reader-WXG3Z7AI.js.map +1 -0
- package/dist/{libav-import-GST2AMPL.cjs → libav-import-2ZVKV2E7.cjs} +2 -2
- package/dist/{libav-import-GST2AMPL.cjs.map → libav-import-2ZVKV2E7.cjs.map} +1 -1
- package/dist/{libav-import-2JURFHEW.js → libav-import-6MGLCXVQ.js} +2 -2
- package/dist/{libav-import-2JURFHEW.js.map → libav-import-6MGLCXVQ.js.map} +1 -1
- package/dist/{player-U2NPmFvA.d.cts → player-B6WB74RD.d.cts} +62 -3
- package/dist/{player-U2NPmFvA.d.ts → player-B6WB74RD.d.ts} +62 -3
- package/dist/player.cjs +5631 -0
- package/dist/player.cjs.map +1 -0
- package/dist/player.d.cts +699 -0
- package/dist/player.d.ts +699 -0
- package/dist/player.js +5629 -0
- package/dist/player.js.map +1 -0
- package/dist/remux-OBSMIENG.cjs +35 -0
- package/dist/remux-OBSMIENG.cjs.map +1 -0
- package/dist/remux-WBYIZBBX.js +10 -0
- package/dist/remux-WBYIZBBX.js.map +1 -0
- package/dist/source-4TZ6KMNV.js +4 -0
- package/dist/{source-FFZ7TW2B.js.map → source-4TZ6KMNV.js.map} +1 -1
- package/dist/source-7YLO6E7X.cjs +29 -0
- package/dist/{source-CN43EI7Z.cjs.map → source-7YLO6E7X.cjs.map} +1 -1
- package/dist/source-MTX5ELUZ.js +4 -0
- package/dist/source-MTX5ELUZ.js.map +1 -0
- package/dist/source-VFLXLOCN.cjs +29 -0
- package/dist/source-VFLXLOCN.cjs.map +1 -0
- package/dist/subtitles-4T74JRGT.js +4 -0
- package/dist/subtitles-4T74JRGT.js.map +1 -0
- package/dist/subtitles-QUH4LPI4.cjs +29 -0
- package/dist/subtitles-QUH4LPI4.cjs.map +1 -0
- package/dist/variant-routing-434STYAB.js +3 -0
- package/dist/{variant-routing-JOBWXYKD.js.map → variant-routing-434STYAB.js.map} +1 -1
- package/dist/variant-routing-HONNAA6R.cjs +12 -0
- package/dist/{variant-routing-GOHB2RZN.cjs.map → variant-routing-HONNAA6R.cjs.map} +1 -1
- package/package.json +9 -1
- package/src/classify/rules.ts +27 -5
- package/src/convert/remux.ts +9 -35
- package/src/convert/transcode-libav.ts +691 -0
- package/src/convert/transcode.ts +53 -12
- package/src/element/avbridge-player.ts +861 -0
- package/src/element/avbridge-video.ts +54 -0
- package/src/element/player-icons.ts +25 -0
- package/src/element/player-styles.ts +472 -0
- package/src/errors.ts +53 -0
- package/src/index.ts +23 -0
- package/src/player-element.ts +18 -0
- package/src/player.ts +118 -27
- package/src/plugins/builtin.ts +2 -2
- package/src/probe/avi.ts +4 -0
- package/src/probe/index.ts +40 -10
- package/src/strategies/fallback/audio-output.ts +31 -0
- package/src/strategies/fallback/decoder.ts +179 -175
- package/src/strategies/fallback/index.ts +48 -6
- package/src/strategies/fallback/libav-import.ts +9 -1
- package/src/strategies/fallback/variant-routing.ts +7 -13
- package/src/strategies/fallback/video-renderer.ts +231 -32
- package/src/strategies/hybrid/decoder.ts +219 -200
- package/src/strategies/hybrid/index.ts +48 -7
- package/src/strategies/native.ts +6 -3
- package/src/strategies/remux/index.ts +14 -2
- package/src/strategies/remux/mse.ts +12 -2
- package/src/strategies/remux/pipeline.ts +72 -12
- package/src/subtitles/index.ts +7 -3
- package/src/subtitles/render.ts +8 -0
- package/src/types.ts +53 -1
- package/src/util/libav-demux.ts +405 -0
- package/src/util/libav-http-reader.ts +5 -1
- package/src/util/source.ts +28 -8
- package/src/util/transport.ts +26 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-6SJLWIWW.cjs.map +0 -1
- package/dist/avi-GCGM7OJI.js.map +0 -1
- package/dist/chunk-DMWARSEF.js.map +0 -1
- package/dist/chunk-HZLQNKFN.cjs.map +0 -1
- package/dist/chunk-ILKDNBSE.js.map +0 -1
- package/dist/chunk-J5MCMN3S.js +0 -27
- package/dist/chunk-J5MCMN3S.js.map +0 -1
- package/dist/chunk-L4NPOJ36.cjs.map +0 -1
- package/dist/chunk-NZU7W256.cjs +0 -29
- package/dist/chunk-NZU7W256.cjs.map +0 -1
- package/dist/chunk-UF2N5L63.cjs.map +0 -1
- package/dist/chunk-WD2ZNQA7.js.map +0 -1
- package/dist/libav-http-reader-FPYDBMYK.cjs +0 -16
- package/dist/libav-http-reader-NQJVY273.js +0 -3
- package/dist/source-CN43EI7Z.cjs +0 -28
- package/dist/source-FFZ7TW2B.js +0 -3
- package/dist/variant-routing-GOHB2RZN.cjs +0 -12
- package/dist/variant-routing-JOBWXYKD.js +0 -3
package/src/player.ts
CHANGED
|
@@ -15,8 +15,10 @@ import type {
|
|
|
15
15
|
PlayerEventMap,
|
|
16
16
|
PlayerEventName,
|
|
17
17
|
StrategyName,
|
|
18
|
+
TransportConfig,
|
|
18
19
|
Listener,
|
|
19
20
|
} from "./types.js";
|
|
21
|
+
import { AvbridgeError, ERR_PLAYER_NOT_READY, ERR_ALL_STRATEGIES_EXHAUSTED } from "./errors.js";
|
|
20
22
|
|
|
21
23
|
export class UnifiedPlayer {
|
|
22
24
|
private emitter = new TypedEmitter<PlayerEventMap>();
|
|
@@ -39,6 +41,15 @@ export class UnifiedPlayer {
|
|
|
39
41
|
// source (e.g. <avbridge-video>).
|
|
40
42
|
private endedListener: (() => void) | null = null;
|
|
41
43
|
|
|
44
|
+
// Background tab handling. userIntent is what the user last asked for
|
|
45
|
+
// (play vs pause) — used to decide whether to auto-resume on visibility
|
|
46
|
+
// return. autoPausedForVisibility tracks whether we paused because the
|
|
47
|
+
// tab was hidden, so we don't resume playback the user deliberately
|
|
48
|
+
// paused (e.g. via media keys while hidden).
|
|
49
|
+
private userIntent: "play" | "pause" = "pause";
|
|
50
|
+
private autoPausedForVisibility = false;
|
|
51
|
+
private visibilityListener: (() => void) | null = null;
|
|
52
|
+
|
|
42
53
|
// Serializes escalation / setStrategy calls
|
|
43
54
|
private switchingPromise: Promise<void> = Promise.resolve();
|
|
44
55
|
|
|
@@ -46,13 +57,23 @@ export class UnifiedPlayer {
|
|
|
46
57
|
// Revoked at destroy() so repeated source swaps don't leak.
|
|
47
58
|
private subtitleResources = new SubtitleResourceBag();
|
|
48
59
|
|
|
60
|
+
// Transport config extracted from CreatePlayerOptions. Threaded to probe,
|
|
61
|
+
// subtitle fetches, and strategy session creators. Not stored on MediaContext
|
|
62
|
+
// because it's runtime config, not media analysis.
|
|
63
|
+
private readonly transport: TransportConfig | undefined;
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* @internal Use {@link createPlayer} or {@link UnifiedPlayer.create} instead.
|
|
51
67
|
*/
|
|
52
68
|
private constructor(
|
|
53
69
|
private readonly options: CreatePlayerOptions,
|
|
54
70
|
private readonly registry: PluginRegistry,
|
|
55
|
-
) {
|
|
71
|
+
) {
|
|
72
|
+
const { requestInit, fetchFn } = options;
|
|
73
|
+
if (requestInit || fetchFn) {
|
|
74
|
+
this.transport = { requestInit, fetchFn };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
56
77
|
|
|
57
78
|
static async create(options: CreatePlayerOptions): Promise<UnifiedPlayer> {
|
|
58
79
|
const registry = new PluginRegistry();
|
|
@@ -74,7 +95,7 @@ export class UnifiedPlayer {
|
|
|
74
95
|
const bootstrapStart = performance.now();
|
|
75
96
|
try {
|
|
76
97
|
dbg.info("bootstrap", "start");
|
|
77
|
-
const ctx = await dbg.timed("probe", "probe", 3000, () => probe(this.options.source));
|
|
98
|
+
const ctx = await dbg.timed("probe", "probe", 3000, () => probe(this.options.source, this.transport));
|
|
78
99
|
dbg.info("probe",
|
|
79
100
|
`container=${ctx.container} video=${ctx.videoTracks[0]?.codec ?? "-"} ` +
|
|
80
101
|
`audio=${ctx.audioTracks[0]?.codec ?? "-"} probedBy=${ctx.probedBy}`,
|
|
@@ -126,21 +147,21 @@ export class UnifiedPlayer {
|
|
|
126
147
|
// Try the primary strategy, falling through the chain on failure
|
|
127
148
|
await this.startSession(decision.strategy, decision.reason);
|
|
128
149
|
|
|
129
|
-
// Apply subtitles for
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
// Apply subtitles for all strategies. Native/remux render them via
|
|
151
|
+
// the inner <video>'s native text-track engine. Hybrid/fallback
|
|
152
|
+
// hide the <video> and render cues into the canvas overlay — see
|
|
153
|
+
// each session's SubtitleOverlay wiring. The <track> elements are
|
|
154
|
+
// attached in both cases so cues are parsed by the browser.
|
|
155
|
+
await attachSubtitleTracks(
|
|
156
|
+
this.options.target,
|
|
157
|
+
ctx.subtitleTracks,
|
|
158
|
+
this.subtitleResources,
|
|
159
|
+
(err, track) => {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.warn(`[avbridge] subtitle ${track.id} failed: ${err.message}`);
|
|
162
|
+
},
|
|
163
|
+
this.transport,
|
|
164
|
+
);
|
|
144
165
|
|
|
145
166
|
this.emitter.emitSticky("tracks", {
|
|
146
167
|
video: ctx.videoTracks,
|
|
@@ -151,6 +172,15 @@ export class UnifiedPlayer {
|
|
|
151
172
|
this.startTimeupdateLoop();
|
|
152
173
|
this.endedListener = () => this.emitter.emit("ended", undefined);
|
|
153
174
|
this.options.target.addEventListener("ended", this.endedListener);
|
|
175
|
+
|
|
176
|
+
// Auto-pause on background tab (unless explicitly opted out).
|
|
177
|
+
// Chrome throttles rAF and setTimeout in hidden tabs, so playback
|
|
178
|
+
// degrades anyway — better to pause cleanly and resume on return.
|
|
179
|
+
if (this.options.backgroundBehavior !== "continue" && typeof document !== "undefined") {
|
|
180
|
+
this.visibilityListener = () => this.onVisibilityChange();
|
|
181
|
+
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
this.emitter.emitSticky("ready", undefined);
|
|
155
185
|
const bootstrapElapsed = performance.now() - bootstrapStart;
|
|
156
186
|
dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
|
|
@@ -181,7 +211,7 @@ export class UnifiedPlayer {
|
|
|
181
211
|
}
|
|
182
212
|
|
|
183
213
|
try {
|
|
184
|
-
this.session = await plugin.execute(this.mediaContext!, this.options.target);
|
|
214
|
+
this.session = await plugin.execute(this.mediaContext!, this.options.target, this.transport);
|
|
185
215
|
} catch (err) {
|
|
186
216
|
// Try the fallback chain
|
|
187
217
|
const chain = this.classification?.fallbackChain;
|
|
@@ -275,7 +305,7 @@ export class UnifiedPlayer {
|
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
try {
|
|
278
|
-
this.session = await plugin.execute(this.mediaContext!, this.options.target);
|
|
308
|
+
this.session = await plugin.execute(this.mediaContext!, this.options.target, this.transport);
|
|
279
309
|
} catch (err) {
|
|
280
310
|
const msg = err instanceof Error ? err.message : String(err);
|
|
281
311
|
errors.push(`${nextStrategy}: ${msg}`);
|
|
@@ -302,8 +332,10 @@ export class UnifiedPlayer {
|
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
// Chain exhausted with no working strategy.
|
|
305
|
-
this.emitter.emit("error", new
|
|
306
|
-
|
|
335
|
+
this.emitter.emit("error", new AvbridgeError(
|
|
336
|
+
ERR_ALL_STRATEGIES_EXHAUSTED,
|
|
337
|
+
`All playback strategies failed: ${errors.join("; ")}`,
|
|
338
|
+
"This file may require a codec or container that isn't available in this browser. Try the fallback strategy or check browser codec support.",
|
|
307
339
|
));
|
|
308
340
|
}
|
|
309
341
|
|
|
@@ -366,7 +398,7 @@ export class UnifiedPlayer {
|
|
|
366
398
|
|
|
367
399
|
/** Manually switch to a different playback strategy. Preserves current position and play/pause state. Concurrent calls are serialized. */
|
|
368
400
|
async setStrategy(strategy: StrategyName, reason?: string): Promise<void> {
|
|
369
|
-
if (!this.mediaContext) throw new
|
|
401
|
+
if (!this.mediaContext) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready — wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
370
402
|
if (this.session?.strategy === strategy) return;
|
|
371
403
|
|
|
372
404
|
this.switchingPromise = this.switchingPromise.then(() =>
|
|
@@ -398,7 +430,7 @@ export class UnifiedPlayer {
|
|
|
398
430
|
const plugin = this.registry.findFor(this.mediaContext!, strategy);
|
|
399
431
|
if (!plugin) throw new Error(`no plugin available for strategy "${strategy}"`);
|
|
400
432
|
|
|
401
|
-
this.session = await plugin.execute(this.mediaContext!, this.options.target);
|
|
433
|
+
this.session = await plugin.execute(this.mediaContext!, this.options.target, this.transport);
|
|
402
434
|
|
|
403
435
|
this.emitter.emitSticky("strategy", {
|
|
404
436
|
strategy,
|
|
@@ -441,30 +473,62 @@ export class UnifiedPlayer {
|
|
|
441
473
|
|
|
442
474
|
/** Begin or resume playback. Throws if the player is not ready. */
|
|
443
475
|
async play(): Promise<void> {
|
|
444
|
-
if (!this.session) throw new
|
|
476
|
+
if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready — wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
477
|
+
this.userIntent = "play";
|
|
478
|
+
this.autoPausedForVisibility = false;
|
|
445
479
|
await this.session.play();
|
|
446
480
|
}
|
|
447
481
|
|
|
448
482
|
/** Pause playback. No-op if the player is not ready or already paused. */
|
|
449
483
|
pause(): void {
|
|
484
|
+
this.userIntent = "pause";
|
|
485
|
+
this.autoPausedForVisibility = false;
|
|
450
486
|
this.session?.pause();
|
|
451
487
|
}
|
|
452
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Handle browser tab visibility changes. On hide: pause if the user
|
|
491
|
+
* had been playing. On show: resume if we were the one who paused.
|
|
492
|
+
* Skips when `backgroundBehavior: "continue"` is set (listener isn't
|
|
493
|
+
* installed in that case).
|
|
494
|
+
*/
|
|
495
|
+
private onVisibilityChange(): void {
|
|
496
|
+
if (!this.session) return;
|
|
497
|
+
const action = decideVisibilityAction({
|
|
498
|
+
hidden: document.hidden,
|
|
499
|
+
userIntent: this.userIntent,
|
|
500
|
+
sessionIsPlaying: !this.options.target.paused,
|
|
501
|
+
autoPausedForVisibility: this.autoPausedForVisibility,
|
|
502
|
+
});
|
|
503
|
+
if (action === "pause") {
|
|
504
|
+
this.autoPausedForVisibility = true;
|
|
505
|
+
dbg.info("visibility", "tab hidden — auto-paused");
|
|
506
|
+
this.session.pause();
|
|
507
|
+
} else if (action === "resume") {
|
|
508
|
+
this.autoPausedForVisibility = false;
|
|
509
|
+
dbg.info("visibility", "tab visible — auto-resuming");
|
|
510
|
+
void this.session.play().catch((err) => {
|
|
511
|
+
// eslint-disable-next-line no-console
|
|
512
|
+
console.warn("[avbridge] auto-resume after tab return failed:", err);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
453
517
|
/** Seek to the given time in seconds. Throws if the player is not ready. */
|
|
454
518
|
async seek(time: number): Promise<void> {
|
|
455
|
-
if (!this.session) throw new
|
|
519
|
+
if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready — wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
456
520
|
await this.session.seek(time);
|
|
457
521
|
}
|
|
458
522
|
|
|
459
523
|
/** Switch the active audio track by track ID. Throws if the player is not ready. */
|
|
460
524
|
async setAudioTrack(id: number): Promise<void> {
|
|
461
|
-
if (!this.session) throw new
|
|
525
|
+
if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready — wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
462
526
|
await this.session.setAudioTrack(id);
|
|
463
527
|
}
|
|
464
528
|
|
|
465
529
|
/** Switch the active subtitle track by track ID, or pass `null` to disable subtitles. */
|
|
466
530
|
async setSubtitleTrack(id: number | null): Promise<void> {
|
|
467
|
-
if (!this.session) throw new
|
|
531
|
+
if (!this.session) throw new AvbridgeError(ERR_PLAYER_NOT_READY, "Player not ready — wait for the 'ready' event before calling playback methods.", "Await the 'ready' event or check player.readyState before calling play/pause/seek.");
|
|
468
532
|
await this.session.setSubtitleTrack(id);
|
|
469
533
|
}
|
|
470
534
|
|
|
@@ -500,6 +564,10 @@ export class UnifiedPlayer {
|
|
|
500
564
|
this.options.target.removeEventListener("ended", this.endedListener);
|
|
501
565
|
this.endedListener = null;
|
|
502
566
|
}
|
|
567
|
+
if (this.visibilityListener) {
|
|
568
|
+
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
569
|
+
this.visibilityListener = null;
|
|
570
|
+
}
|
|
503
571
|
if (this.session) {
|
|
504
572
|
await this.session.destroy();
|
|
505
573
|
this.session = null;
|
|
@@ -515,6 +583,29 @@ export async function createPlayer(options: CreatePlayerOptions): Promise<Unifie
|
|
|
515
583
|
return UnifiedPlayer.create(options);
|
|
516
584
|
}
|
|
517
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Pure decision function for visibility-change handling. Separated from
|
|
588
|
+
* the class method so it can be unit-tested without a full player
|
|
589
|
+
* instance.
|
|
590
|
+
*
|
|
591
|
+
* @internal — exported for unit tests; not part of the public API.
|
|
592
|
+
*/
|
|
593
|
+
export function decideVisibilityAction(state: {
|
|
594
|
+
hidden: boolean;
|
|
595
|
+
userIntent: "play" | "pause";
|
|
596
|
+
sessionIsPlaying: boolean;
|
|
597
|
+
autoPausedForVisibility: boolean;
|
|
598
|
+
}): "pause" | "resume" | "noop" {
|
|
599
|
+
if (state.hidden) {
|
|
600
|
+
// Tab hidden: pause if user had been playing and session is active
|
|
601
|
+
if (state.userIntent === "play" && state.sessionIsPlaying) return "pause";
|
|
602
|
+
return "noop";
|
|
603
|
+
}
|
|
604
|
+
// Tab visible: resume only if we're the one who paused
|
|
605
|
+
if (state.autoPausedForVisibility) return "resume";
|
|
606
|
+
return "noop";
|
|
607
|
+
}
|
|
608
|
+
|
|
518
609
|
/**
|
|
519
610
|
* Build a synthetic classification for an explicit `initialStrategy` override.
|
|
520
611
|
* The `class` is derived from the chosen strategy so diagnostics and any
|
package/src/plugins/builtin.ts
CHANGED
|
@@ -20,13 +20,13 @@ const remuxPlugin: Plugin = {
|
|
|
20
20
|
const hybridPlugin: Plugin = {
|
|
21
21
|
name: "hybrid",
|
|
22
22
|
canHandle: () => typeof VideoDecoder !== "undefined",
|
|
23
|
-
execute: (ctx, video) => createHybridSession(ctx, video),
|
|
23
|
+
execute: (ctx, video, transport) => createHybridSession(ctx, video, transport),
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const fallbackPlugin: Plugin = {
|
|
27
27
|
name: "fallback",
|
|
28
28
|
canHandle: () => true,
|
|
29
|
-
execute: (ctx, video) => createFallbackSession(ctx, video),
|
|
29
|
+
execute: (ctx, video, transport) => createFallbackSession(ctx, video, transport),
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
export function registerBuiltins(registry: PluginRegistry): void {
|
package/src/probe/avi.ts
CHANGED
|
@@ -205,6 +205,10 @@ function ffmpegToAvbridgeAudio(name: string): AudioCodec {
|
|
|
205
205
|
case "ra_288": return "ra_288";
|
|
206
206
|
case "sipr": return "sipr";
|
|
207
207
|
case "atrac3": return "atrac3";
|
|
208
|
+
case "dca":
|
|
209
|
+
case "dts": return "dts";
|
|
210
|
+
case "truehd":
|
|
211
|
+
case "mlp": return "truehd";
|
|
208
212
|
default: return name as AudioCodec;
|
|
209
213
|
}
|
|
210
214
|
}
|
package/src/probe/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ContainerKind, MediaContext, MediaInput } from "../types.js";
|
|
1
|
+
import type { ContainerKind, MediaContext, MediaInput, TransportConfig } from "../types.js";
|
|
2
2
|
import { normalizeSource, sniffNormalizedSource } from "../util/source.js";
|
|
3
3
|
import { probeWithMediabunny } from "./mediabunny.js";
|
|
4
|
+
import { AvbridgeError, ERR_PROBE_FAILED, ERR_PROBE_UNKNOWN_CONTAINER, ERR_LIBAV_NOT_REACHABLE } from "../errors.js";
|
|
4
5
|
|
|
5
6
|
/** Containers mediabunny can demux. Sniff results outside this set go straight to libav. */
|
|
6
7
|
const MEDIABUNNY_CONTAINERS = new Set<ContainerKind>([
|
|
@@ -32,13 +33,33 @@ const MEDIABUNNY_CONTAINERS = new Set<ContainerKind>([
|
|
|
32
33
|
* mediabunny can't read those containers at all, so there's no fast path
|
|
33
34
|
* to try.
|
|
34
35
|
*/
|
|
35
|
-
export async function probe(
|
|
36
|
-
|
|
36
|
+
export async function probe(
|
|
37
|
+
source: MediaInput,
|
|
38
|
+
transport?: TransportConfig,
|
|
39
|
+
): Promise<MediaContext> {
|
|
40
|
+
const normalized = await normalizeSource(source, transport);
|
|
37
41
|
const sniffed = await sniffNormalizedSource(normalized);
|
|
38
42
|
|
|
39
43
|
if (MEDIABUNNY_CONTAINERS.has(sniffed)) {
|
|
40
44
|
try {
|
|
41
|
-
|
|
45
|
+
const result = await probeWithMediabunny(normalized, sniffed);
|
|
46
|
+
// If mediabunny returned unknown codecs, try libav which recognizes
|
|
47
|
+
// a wider range (DTS, TrueHD, etc.). Only escalate if there ARE
|
|
48
|
+
// tracks with unknown codecs — otherwise mediabunny's result is fine.
|
|
49
|
+
const hasUnknownCodec =
|
|
50
|
+
result.videoTracks.some((t) => t.codec === "unknown") ||
|
|
51
|
+
result.audioTracks.some((t) => t.codec === "unknown");
|
|
52
|
+
if (hasUnknownCodec) {
|
|
53
|
+
try {
|
|
54
|
+
const { probeWithLibav } = await import("./avi.js");
|
|
55
|
+
return await probeWithLibav(normalized, sniffed);
|
|
56
|
+
} catch {
|
|
57
|
+
// libav also failed — return mediabunny's result (unknown codecs
|
|
58
|
+
// are better than no result at all)
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
42
63
|
} catch (mediabunnyErr) {
|
|
43
64
|
// mediabunny rejected the file. Before giving up, try libav — it can
|
|
44
65
|
// demux a much wider range of codec combinations in ISOBMFF/MKV/etc.
|
|
@@ -56,8 +77,10 @@ export async function probe(source: MediaInput): Promise<MediaContext> {
|
|
|
56
77
|
} catch (libavErr) {
|
|
57
78
|
const mbMsg = (mediabunnyErr as Error).message || String(mediabunnyErr);
|
|
58
79
|
const lvMsg = libavErr instanceof Error ? libavErr.message : String(libavErr);
|
|
59
|
-
throw new
|
|
60
|
-
|
|
80
|
+
throw new AvbridgeError(
|
|
81
|
+
ERR_PROBE_FAILED,
|
|
82
|
+
`Failed to probe ${sniffed.toUpperCase()} file. mediabunny: ${mbMsg}. libav: ${lvMsg}.`,
|
|
83
|
+
"The file may be corrupt, truncated, or in an unsupported format. Enable AVBRIDGE_DEBUG for detailed logs.",
|
|
61
84
|
);
|
|
62
85
|
}
|
|
63
86
|
}
|
|
@@ -71,10 +94,17 @@ export async function probe(source: MediaInput): Promise<MediaContext> {
|
|
|
71
94
|
const inner = err instanceof Error ? err.message : String(err);
|
|
72
95
|
// eslint-disable-next-line no-console
|
|
73
96
|
console.error("[avbridge] libav probe failed for", sniffed, "file:", err);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:
|
|
97
|
+
if (sniffed === "unknown") {
|
|
98
|
+
throw new AvbridgeError(
|
|
99
|
+
ERR_PROBE_UNKNOWN_CONTAINER,
|
|
100
|
+
`Unable to probe source: container format could not be identified. libav fallback: ${inner || "(no details)"}`,
|
|
101
|
+
"The file may be corrupt or in a format avbridge doesn't recognize. Check the file plays in VLC or ffprobe.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
throw new AvbridgeError(
|
|
105
|
+
ERR_LIBAV_NOT_REACHABLE,
|
|
106
|
+
`${sniffed.toUpperCase()} files require libav.js, which failed to load: ${inner || "(no details)"}`,
|
|
107
|
+
"Install @libav.js/variant-webcodecs, or check that AVBRIDGE_LIBAV_BASE points to the correct path.",
|
|
78
108
|
);
|
|
79
109
|
}
|
|
80
110
|
}
|
|
@@ -76,12 +76,42 @@ export class AudioOutput implements ClockSource {
|
|
|
76
76
|
private framesScheduled = 0;
|
|
77
77
|
private destroyed = false;
|
|
78
78
|
|
|
79
|
+
/** User-set volume (0..1). Applied to the gain node. */
|
|
80
|
+
private _volume = 1;
|
|
81
|
+
/** User-set muted flag. When true, gain is forced to 0. */
|
|
82
|
+
private _muted = false;
|
|
83
|
+
|
|
79
84
|
constructor() {
|
|
80
85
|
this.ctx = new AudioContext();
|
|
81
86
|
this.gain = this.ctx.createGain();
|
|
82
87
|
this.gain.connect(this.ctx.destination);
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/** Set volume (0..1). Applied immediately to the gain node. */
|
|
91
|
+
setVolume(v: number): void {
|
|
92
|
+
this._volume = Math.max(0, Math.min(1, v));
|
|
93
|
+
this.applyGain();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getVolume(): number {
|
|
97
|
+
return this._volume;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Set muted. When true, output is silenced regardless of volume. */
|
|
101
|
+
setMuted(m: boolean): void {
|
|
102
|
+
this._muted = m;
|
|
103
|
+
this.applyGain();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getMuted(): boolean {
|
|
107
|
+
return this._muted;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private applyGain(): void {
|
|
111
|
+
const target = this._muted ? 0 : this._volume;
|
|
112
|
+
try { this.gain.gain.value = target; } catch { /* ignore */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
/**
|
|
86
116
|
* Switch into wall-clock fallback mode. Called by the decoder when no
|
|
87
117
|
* audio decoder could be initialized for the source. Once set, this
|
|
@@ -287,6 +317,7 @@ export class AudioOutput implements ClockSource {
|
|
|
287
317
|
try { this.gain.disconnect(); } catch { /* ignore */ }
|
|
288
318
|
this.gain = this.ctx.createGain();
|
|
289
319
|
this.gain.connect(this.ctx.destination);
|
|
320
|
+
this.applyGain();
|
|
290
321
|
|
|
291
322
|
this.pendingQueue = [];
|
|
292
323
|
this.mediaTimeOfAnchor = newMediaTime;
|