avbridge 2.6.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +23 -0
- package/dist/{chunk-6SOFJV44.cjs → chunk-IUSFLVLJ.cjs} +21 -6
- package/dist/chunk-IUSFLVLJ.cjs.map +1 -0
- package/dist/{chunk-OGYHFY6K.js → chunk-JSQOBUQB.js} +21 -6
- package/dist/chunk-JSQOBUQB.js.map +1 -0
- package/dist/element-browser.js +146 -7
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +129 -5
- package/dist/element.cjs.map +1 -1
- package/dist/element.d.cts +52 -3
- package/dist/element.d.ts +52 -3
- package/dist/element.js +128 -4
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{player-DGXeCNfD.d.cts → player-DXEKOky8.d.cts} +3 -0
- package/dist/{player-DGXeCNfD.d.ts → player-DXEKOky8.d.ts} +3 -0
- package/dist/player.cjs +222 -11
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +61 -3
- package/dist/player.d.ts +61 -3
- package/dist/player.js +222 -11
- package/dist/player.js.map +1 -1
- package/package.json +7 -1
- package/src/classify/rules.ts +23 -0
- package/src/element/avbridge-player.ts +40 -5
- package/src/element/avbridge-video.ts +148 -4
- package/src/element/player-styles.ts +44 -0
- package/src/element.ts +3 -3
- package/src/strategies/fallback/decoder.ts +14 -7
- package/src/strategies/fallback/video-renderer.ts +7 -5
- package/src/types.ts +1 -0
- package/dist/chunk-6SOFJV44.cjs.map +0 -1
- package/dist/chunk-OGYHFY6K.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AudioCodec, V as VideoCodec, M as MediaContext, C as Classification, a as MediaInput, T as TransportConfig, b as ConvertOptions, c as ConvertResult, d as TranscodeOptions } from './player-
|
|
2
|
-
export { e as AudioTrackInfo, f as ContainerKind, g as CreatePlayerOptions, D as DiagnosticsSnapshot, F as FetchFn, H as HardwareAccelerationHint, O as OutputAudioCodec, h as OutputFormat, i as OutputVideoCodec, P as PlaybackSession, j as PlayerEventMap, k as PlayerEventName, l as Plugin, m as ProgressInfo, S as StrategyClass, n as StrategyName, o as SubtitleTrackInfo, p as TranscodeQuality, U as UnifiedPlayer, q as VideoTrackInfo, r as createPlayer } from './player-
|
|
1
|
+
import { A as AudioCodec, V as VideoCodec, M as MediaContext, C as Classification, a as MediaInput, T as TransportConfig, b as ConvertOptions, c as ConvertResult, d as TranscodeOptions } from './player-DXEKOky8.cjs';
|
|
2
|
+
export { e as AudioTrackInfo, f as ContainerKind, g as CreatePlayerOptions, D as DiagnosticsSnapshot, F as FetchFn, H as HardwareAccelerationHint, O as OutputAudioCodec, h as OutputFormat, i as OutputVideoCodec, P as PlaybackSession, j as PlayerEventMap, k as PlayerEventName, l as Plugin, m as ProgressInfo, S as StrategyClass, n as StrategyName, o as SubtitleTrackInfo, p as TranscodeQuality, U as UnifiedPlayer, q as VideoTrackInfo, r as createPlayer } from './player-DXEKOky8.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Codecs we know `<video>` and MSE support across modern desktop + Android.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AudioCodec, V as VideoCodec, M as MediaContext, C as Classification, a as MediaInput, T as TransportConfig, b as ConvertOptions, c as ConvertResult, d as TranscodeOptions } from './player-
|
|
2
|
-
export { e as AudioTrackInfo, f as ContainerKind, g as CreatePlayerOptions, D as DiagnosticsSnapshot, F as FetchFn, H as HardwareAccelerationHint, O as OutputAudioCodec, h as OutputFormat, i as OutputVideoCodec, P as PlaybackSession, j as PlayerEventMap, k as PlayerEventName, l as Plugin, m as ProgressInfo, S as StrategyClass, n as StrategyName, o as SubtitleTrackInfo, p as TranscodeQuality, U as UnifiedPlayer, q as VideoTrackInfo, r as createPlayer } from './player-
|
|
1
|
+
import { A as AudioCodec, V as VideoCodec, M as MediaContext, C as Classification, a as MediaInput, T as TransportConfig, b as ConvertOptions, c as ConvertResult, d as TranscodeOptions } from './player-DXEKOky8.js';
|
|
2
|
+
export { e as AudioTrackInfo, f as ContainerKind, g as CreatePlayerOptions, D as DiagnosticsSnapshot, F as FetchFn, H as HardwareAccelerationHint, O as OutputAudioCodec, h as OutputFormat, i as OutputVideoCodec, P as PlaybackSession, j as PlayerEventMap, k as PlayerEventName, l as Plugin, m as ProgressInfo, S as StrategyClass, n as StrategyName, o as SubtitleTrackInfo, p as TranscodeQuality, U as UnifiedPlayer, q as VideoTrackInfo, r as createPlayer } from './player-DXEKOky8.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Codecs we know `<video>` and MSE support across modern desktop + Android.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mimeForFormat, generateFilename, createOutputFormat } from './chunk-SMH6IOP2.js';
|
|
2
2
|
export { remux } from './chunk-SMH6IOP2.js';
|
|
3
|
-
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-
|
|
3
|
+
export { FALLBACK_AUDIO_CODECS, FALLBACK_VIDEO_CODECS, NATIVE_AUDIO_CODECS, NATIVE_VIDEO_CODECS, UnifiedPlayer, classifyContext as classify, createPlayer } from './chunk-JSQOBUQB.js';
|
|
4
4
|
export { srtToVtt } from './chunk-5KVLE6YI.js';
|
|
5
5
|
import { probe, buildMediabunnySourceFromInput } from './chunk-SR3MPV4D.js';
|
|
6
6
|
export { probe } from './chunk-SR3MPV4D.js';
|
|
@@ -293,6 +293,9 @@ interface AvbridgeVideoElementEventMap {
|
|
|
293
293
|
buffered: TimeRanges;
|
|
294
294
|
}>;
|
|
295
295
|
loadstart: CustomEvent<Record<string, never>>;
|
|
296
|
+
fitchange: CustomEvent<{
|
|
297
|
+
fit: "contain" | "cover" | "fill";
|
|
298
|
+
}>;
|
|
296
299
|
}
|
|
297
300
|
/** Target output format for conversion functions. */
|
|
298
301
|
type OutputFormat = "mp4" | "webm" | "mkv";
|
|
@@ -293,6 +293,9 @@ interface AvbridgeVideoElementEventMap {
|
|
|
293
293
|
buffered: TimeRanges;
|
|
294
294
|
}>;
|
|
295
295
|
loadstart: CustomEvent<Record<string, never>>;
|
|
296
|
+
fitchange: CustomEvent<{
|
|
297
|
+
fit: "contain" | "cover" | "fill";
|
|
298
|
+
}>;
|
|
296
299
|
}
|
|
297
300
|
/** Target output format for conversion functions. */
|
|
298
301
|
type OutputFormat = "mp4" | "webm" | "mkv";
|
package/dist/player.cjs
CHANGED
|
@@ -496,6 +496,22 @@ function classifyContext(ctx) {
|
|
|
496
496
|
};
|
|
497
497
|
}
|
|
498
498
|
if (REMUXABLE_CONTAINERS.has(ctx.container)) {
|
|
499
|
+
const mime = mp4MimeFor(video, audio);
|
|
500
|
+
if (mime && typeof MediaSource !== "undefined" && !mseSupports(mime)) {
|
|
501
|
+
if (webCodecsAvailable()) {
|
|
502
|
+
return {
|
|
503
|
+
class: "HYBRID_CANDIDATE",
|
|
504
|
+
strategy: "hybrid",
|
|
505
|
+
reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime \u2014 routing to WebCodecs hardware decode`,
|
|
506
|
+
fallbackChain: ["fallback"]
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
class: "FALLBACK_REQUIRED",
|
|
511
|
+
strategy: "fallback",
|
|
512
|
+
reason: `${ctx.container} container with ${video.codec}${audio ? "/" + audio.codec : ""}; MSE rejects the remux target mime and WebCodecs is unavailable \u2014 falling back to WASM decode`
|
|
513
|
+
};
|
|
514
|
+
}
|
|
499
515
|
return {
|
|
500
516
|
class: "REMUX_CANDIDATE",
|
|
501
517
|
strategy: "remux",
|
|
@@ -1239,7 +1255,7 @@ var VideoRenderer = class {
|
|
|
1239
1255
|
this.resolveFirstFrame = resolve;
|
|
1240
1256
|
});
|
|
1241
1257
|
this.canvas = document.createElement("canvas");
|
|
1242
|
-
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
|
|
1258
|
+
this.canvas.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:var(--avbridge-fit, contain);";
|
|
1243
1259
|
const parent = target.parentElement ?? target.parentNode;
|
|
1244
1260
|
if (parent && parent instanceof HTMLElement) {
|
|
1245
1261
|
if (getComputedStyle(parent).position === "static") {
|
|
@@ -2582,7 +2598,7 @@ async function createHybridSession(ctx, target, transport) {
|
|
|
2582
2598
|
|
|
2583
2599
|
// src/strategies/fallback/decoder.ts
|
|
2584
2600
|
async function startDecoder(opts) {
|
|
2585
|
-
const variant =
|
|
2601
|
+
const variant = "avbridge";
|
|
2586
2602
|
const libav = await chunkNNVOHKXJ_cjs.loadLibav(variant);
|
|
2587
2603
|
const bridge = await loadBridge2();
|
|
2588
2604
|
const { prepareLibavInput } = await import('./libav-http-reader-AZLE7YFS.cjs');
|
|
@@ -2638,9 +2654,8 @@ async function startDecoder(opts) {
|
|
|
2638
2654
|
videoStream ? `video: ${opts.context.videoTracks[0]?.codec ?? "unknown"}` : null,
|
|
2639
2655
|
audioStream ? `audio: ${opts.context.audioTracks[0]?.codec ?? "unknown"}` : null
|
|
2640
2656
|
].filter(Boolean).join(", ");
|
|
2641
|
-
const hint = variant === "webcodecs" ? ` The "${variant}" libav variant does not include software decoders for these codecs. Try the custom "avbridge" variant (scripts/build-libav.sh) for broader codec support, or use a lighter strategy (native, remux, hybrid) instead.` : "";
|
|
2642
2657
|
throw new Error(
|
|
2643
|
-
`fallback decoder: could not initialize any libav decoders (${codecs})
|
|
2658
|
+
`fallback decoder: could not initialize any libav decoders (${codecs}). The "${variant}" libav variant lacks software decoders for these codecs \u2014 rebuild with scripts/build-libav.sh including the missing decoder, or use a lighter strategy (native, remux, hybrid) instead.`
|
|
2644
2659
|
);
|
|
2645
2660
|
}
|
|
2646
2661
|
let bsfCtx = null;
|
|
@@ -3767,6 +3782,8 @@ var PREFERRED_STRATEGY_VALUES = /* @__PURE__ */ new Set([
|
|
|
3767
3782
|
"hybrid",
|
|
3768
3783
|
"fallback"
|
|
3769
3784
|
]);
|
|
3785
|
+
var FIT_VALUES = /* @__PURE__ */ new Set(["contain", "cover", "fill"]);
|
|
3786
|
+
var DEFAULT_FIT = "contain";
|
|
3770
3787
|
var FORWARDED_VIDEO_EVENTS = [
|
|
3771
3788
|
"loadstart",
|
|
3772
3789
|
"loadedmetadata",
|
|
@@ -3801,7 +3818,9 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3801
3818
|
"crossorigin",
|
|
3802
3819
|
"disableremoteplayback",
|
|
3803
3820
|
"diagnostics",
|
|
3804
|
-
"preferstrategy"
|
|
3821
|
+
"preferstrategy",
|
|
3822
|
+
"fit",
|
|
3823
|
+
"no-orientation-lock"
|
|
3805
3824
|
];
|
|
3806
3825
|
// ── Internal state ─────────────────────────────────────────────────────
|
|
3807
3826
|
/** The shadow DOM `<video>` element that strategies render into. */
|
|
@@ -3853,23 +3872,38 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3853
3872
|
* native fails.
|
|
3854
3873
|
*/
|
|
3855
3874
|
_preferredStrategy = "auto";
|
|
3875
|
+
/** Current fit mode. Applied to the inner `<video>` via object-fit, and
|
|
3876
|
+
* to the fallback canvas via the `--avbridge-fit` CSS custom property on
|
|
3877
|
+
* the stage wrapper (see `src/strategies/fallback/video-renderer.ts`). */
|
|
3878
|
+
_fit = DEFAULT_FIT;
|
|
3879
|
+
/** The stage wrapper — the element the canvas attaches into, and where
|
|
3880
|
+
* the `--avbridge-fit` CSS custom property lives. */
|
|
3881
|
+
_stageEl;
|
|
3856
3882
|
/** Set if currentTime was assigned before the player was ready. */
|
|
3857
3883
|
_pendingSeek = null;
|
|
3858
3884
|
/** Set if play() was called before the player was ready. */
|
|
3859
3885
|
_pendingPlay = false;
|
|
3860
3886
|
/** MutationObserver tracking light-DOM `<track>` children. */
|
|
3861
3887
|
_trackObserver = null;
|
|
3888
|
+
/** Document-level fullscreenchange handler — installed while connected so
|
|
3889
|
+
* the element can lock/unlock screen orientation to match the video's
|
|
3890
|
+
* intrinsic aspect. */
|
|
3891
|
+
_fullscreenChangeHandler = null;
|
|
3892
|
+
/** True if we successfully called screen.orientation.lock() on the last
|
|
3893
|
+
* fullscreen entry. Used to know whether to unlock on exit. */
|
|
3894
|
+
_orientationLocked = false;
|
|
3862
3895
|
// ── Construction & lifecycle ───────────────────────────────────────────
|
|
3863
3896
|
constructor() {
|
|
3864
3897
|
super();
|
|
3865
3898
|
const root = this.attachShadow({ mode: "open" });
|
|
3866
3899
|
const stage = document.createElement("div");
|
|
3867
3900
|
stage.setAttribute("part", "stage");
|
|
3868
|
-
stage.style.cssText =
|
|
3901
|
+
stage.style.cssText = `position:relative;width:100%;height:100%;display:block;--avbridge-fit:${DEFAULT_FIT};`;
|
|
3869
3902
|
root.appendChild(stage);
|
|
3903
|
+
this._stageEl = stage;
|
|
3870
3904
|
this._videoEl = document.createElement("video");
|
|
3871
3905
|
this._videoEl.setAttribute("part", "video");
|
|
3872
|
-
this._videoEl.style.cssText =
|
|
3906
|
+
this._videoEl.style.cssText = `width:100%;height:100%;display:block;background:#000;object-fit:var(--avbridge-fit, ${DEFAULT_FIT});`;
|
|
3873
3907
|
this._videoEl.playsInline = true;
|
|
3874
3908
|
stage.appendChild(this._videoEl);
|
|
3875
3909
|
this._videoEl.addEventListener("progress", () => {
|
|
@@ -3890,6 +3924,10 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3890
3924
|
this._trackObserver = new MutationObserver(() => this._syncTextTracks());
|
|
3891
3925
|
this._trackObserver.observe(this, { childList: true, subtree: false });
|
|
3892
3926
|
}
|
|
3927
|
+
if (!this._fullscreenChangeHandler) {
|
|
3928
|
+
this._fullscreenChangeHandler = () => this._onFullscreenChange();
|
|
3929
|
+
document.addEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
3930
|
+
}
|
|
3893
3931
|
const source = this._activeSource();
|
|
3894
3932
|
if (source != null) {
|
|
3895
3933
|
void this._bootstrap(source);
|
|
@@ -3901,6 +3939,11 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3901
3939
|
this._trackObserver.disconnect();
|
|
3902
3940
|
this._trackObserver = null;
|
|
3903
3941
|
}
|
|
3942
|
+
if (this._fullscreenChangeHandler) {
|
|
3943
|
+
document.removeEventListener("fullscreenchange", this._fullscreenChangeHandler);
|
|
3944
|
+
this._fullscreenChangeHandler = null;
|
|
3945
|
+
}
|
|
3946
|
+
this._releaseOrientationLock();
|
|
3904
3947
|
this._bootstrapId++;
|
|
3905
3948
|
void this._teardown();
|
|
3906
3949
|
}
|
|
@@ -3934,6 +3977,14 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
3934
3977
|
this._preferredStrategy = "auto";
|
|
3935
3978
|
}
|
|
3936
3979
|
break;
|
|
3980
|
+
case "fit": {
|
|
3981
|
+
const next = newValue && FIT_VALUES.has(newValue) ? newValue : DEFAULT_FIT;
|
|
3982
|
+
if (next === this._fit) break;
|
|
3983
|
+
this._fit = next;
|
|
3984
|
+
this._stageEl.style.setProperty("--avbridge-fit", next);
|
|
3985
|
+
this._dispatch("fitchange", { fit: next });
|
|
3986
|
+
break;
|
|
3987
|
+
}
|
|
3937
3988
|
}
|
|
3938
3989
|
}
|
|
3939
3990
|
// ── Source handling ────────────────────────────────────────────────────
|
|
@@ -4175,6 +4226,13 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4175
4226
|
if (value) this.setAttribute("diagnostics", "");
|
|
4176
4227
|
else this.removeAttribute("diagnostics");
|
|
4177
4228
|
}
|
|
4229
|
+
get fit() {
|
|
4230
|
+
return this._fit;
|
|
4231
|
+
}
|
|
4232
|
+
set fit(value) {
|
|
4233
|
+
if (!FIT_VALUES.has(value)) return;
|
|
4234
|
+
this.setAttribute("fit", value);
|
|
4235
|
+
}
|
|
4178
4236
|
get preferredStrategy() {
|
|
4179
4237
|
return this._preferredStrategy;
|
|
4180
4238
|
}
|
|
@@ -4348,6 +4406,87 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
|
|
|
4348
4406
|
}
|
|
4349
4407
|
);
|
|
4350
4408
|
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Disable the automatic `screen.orientation.lock()` that runs on
|
|
4411
|
+
* fullscreen entry. Set when you want to honor the device's native
|
|
4412
|
+
* auto-rotate instead of matching the video's intrinsic orientation.
|
|
4413
|
+
*/
|
|
4414
|
+
get noOrientationLock() {
|
|
4415
|
+
return this.hasAttribute("no-orientation-lock");
|
|
4416
|
+
}
|
|
4417
|
+
set noOrientationLock(value) {
|
|
4418
|
+
if (value) this.setAttribute("no-orientation-lock", "");
|
|
4419
|
+
else this.removeAttribute("no-orientation-lock");
|
|
4420
|
+
}
|
|
4421
|
+
// ── Fullscreen orientation lock ────────────────────────────────────────
|
|
4422
|
+
/** Called whenever `document.fullscreenchange` fires. If this element (or
|
|
4423
|
+
* any of its ancestors) is now fullscreen, derive the target orientation
|
|
4424
|
+
* from the video's intrinsic size and call `screen.orientation.lock()`.
|
|
4425
|
+
* On exit, release the lock we took. iOS Safari rejects `lock()` — we
|
|
4426
|
+
* swallow the rejection so nothing breaks on that path. */
|
|
4427
|
+
_onFullscreenChange() {
|
|
4428
|
+
if (this._destroyed) return;
|
|
4429
|
+
const fsEl = document.fullscreenElement;
|
|
4430
|
+
const nowFullscreen = fsEl != null && this._isInsideOrEquals(fsEl);
|
|
4431
|
+
if (nowFullscreen && !this._orientationLocked) {
|
|
4432
|
+
if (this.noOrientationLock) return;
|
|
4433
|
+
const target = this._desiredOrientation();
|
|
4434
|
+
if (!target) return;
|
|
4435
|
+
void this._lockOrientation(target);
|
|
4436
|
+
} else if (!nowFullscreen && this._orientationLocked) {
|
|
4437
|
+
this._releaseOrientationLock();
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
/** Walk composed-tree ancestors to see if `target` is this element or
|
|
4441
|
+
* any ancestor across shadow boundaries. `Node.contains()` can't cross
|
|
4442
|
+
* shadow roots, so when `<avbridge-player>` (the fullscreen element)
|
|
4443
|
+
* hosts this `<avbridge-video>` inside its shadow DOM, `contains()`
|
|
4444
|
+
* returns false. */
|
|
4445
|
+
_isInsideOrEquals(target) {
|
|
4446
|
+
let node = this;
|
|
4447
|
+
while (node) {
|
|
4448
|
+
if (node === target) return true;
|
|
4449
|
+
const parent = node.parentNode;
|
|
4450
|
+
if (parent instanceof ShadowRoot) node = parent.host;
|
|
4451
|
+
else node = parent;
|
|
4452
|
+
}
|
|
4453
|
+
return false;
|
|
4454
|
+
}
|
|
4455
|
+
/** Derive "landscape" / "portrait" from the intrinsic video dimensions.
|
|
4456
|
+
* Returns null when dimensions aren't known yet or the video is square.
|
|
4457
|
+
* Uses `videoWidth` / `videoHeight` from the inner `<video>`, which the
|
|
4458
|
+
* browser sets to the display-aspect-corrected size (so anamorphic
|
|
4459
|
+
* content is judged by its display aspect, not pixel aspect). */
|
|
4460
|
+
_desiredOrientation() {
|
|
4461
|
+
const w = this._videoEl.videoWidth;
|
|
4462
|
+
const h = this._videoEl.videoHeight;
|
|
4463
|
+
if (!w || !h) return null;
|
|
4464
|
+
if (w === h) return null;
|
|
4465
|
+
return w > h ? "landscape" : "portrait";
|
|
4466
|
+
}
|
|
4467
|
+
/** Attempt to lock screen orientation. Swallows rejections — iOS Safari
|
|
4468
|
+
* doesn't implement `lock()`, and desktop / non-fullscreen contexts will
|
|
4469
|
+
* reject too. Records success so we know whether to unlock on exit. */
|
|
4470
|
+
async _lockOrientation(target) {
|
|
4471
|
+
const so = screen.orientation;
|
|
4472
|
+
if (!so || typeof so.lock !== "function") return;
|
|
4473
|
+
try {
|
|
4474
|
+
await so.lock(target);
|
|
4475
|
+
this._orientationLocked = true;
|
|
4476
|
+
} catch {
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
_releaseOrientationLock() {
|
|
4480
|
+
if (!this._orientationLocked) return;
|
|
4481
|
+
this._orientationLocked = false;
|
|
4482
|
+
const so = screen.orientation;
|
|
4483
|
+
if (so && typeof so.unlock === "function") {
|
|
4484
|
+
try {
|
|
4485
|
+
so.unlock();
|
|
4486
|
+
} catch {
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4351
4490
|
// ── Public methods ─────────────────────────────────────────────────────
|
|
4352
4491
|
/** Force a (re-)bootstrap if a source is currently set. */
|
|
4353
4492
|
async load() {
|
|
@@ -4607,6 +4746,50 @@ var PLAYER_STYLES = (
|
|
|
4607
4746
|
|
|
4608
4747
|
:host([data-controls-hidden]) { cursor: none; }
|
|
4609
4748
|
|
|
4749
|
+
/* \u2500\u2500 Top toolbar (slotted consumer chrome) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4750
|
+
Two named slots (top-left, top-right) let consumers place back / title /
|
|
4751
|
+
translate buttons inside the auto-hide chrome. Wrapper has
|
|
4752
|
+
pointer-events:none so empty slots don't block container clicks; each
|
|
4753
|
+
side re-enables pointer-events so real buttons remain interactive. */
|
|
4754
|
+
|
|
4755
|
+
.avp-toolbar-top {
|
|
4756
|
+
position: absolute;
|
|
4757
|
+
top: 0;
|
|
4758
|
+
left: 0;
|
|
4759
|
+
right: 0;
|
|
4760
|
+
z-index: 5;
|
|
4761
|
+
padding: 8px 12px 24px;
|
|
4762
|
+
background: linear-gradient(rgba(0, 0, 0, 0.6), transparent);
|
|
4763
|
+
display: flex;
|
|
4764
|
+
align-items: flex-start;
|
|
4765
|
+
justify-content: space-between;
|
|
4766
|
+
gap: 8px;
|
|
4767
|
+
opacity: 1;
|
|
4768
|
+
pointer-events: none;
|
|
4769
|
+
transition: opacity 0.25s;
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
.avp-toolbar-top-left,
|
|
4773
|
+
.avp-toolbar-top-right {
|
|
4774
|
+
display: flex;
|
|
4775
|
+
align-items: center;
|
|
4776
|
+
gap: 8px;
|
|
4777
|
+
pointer-events: auto;
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
.avp-toolbar-top-right { margin-left: auto; }
|
|
4781
|
+
|
|
4782
|
+
/* Hide the gradient band when no consumer has slotted anything \u2014 we
|
|
4783
|
+
toggle data-toolbar-empty from JS via slotchange. */
|
|
4784
|
+
:host([data-toolbar-empty]) .avp-toolbar-top {
|
|
4785
|
+
background: none;
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4788
|
+
:host([data-controls-hidden]) .avp-toolbar-top {
|
|
4789
|
+
opacity: 0;
|
|
4790
|
+
pointer-events: none;
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4610
4793
|
/* \u2500\u2500 Seek bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4611
4794
|
|
|
4612
4795
|
.avp-seek {
|
|
@@ -4958,7 +5141,8 @@ var PROXY_ATTRIBUTES = [
|
|
|
4958
5141
|
"playsinline",
|
|
4959
5142
|
"crossorigin",
|
|
4960
5143
|
"disableremoteplayback",
|
|
4961
|
-
"preferstrategy"
|
|
5144
|
+
"preferstrategy",
|
|
5145
|
+
"fit"
|
|
4962
5146
|
];
|
|
4963
5147
|
var AvbridgePlayerElement = class extends HTMLElement {
|
|
4964
5148
|
static observedAttributes = [...PROXY_ATTRIBUTES];
|
|
@@ -4996,6 +5180,8 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
4996
5180
|
_statsEl;
|
|
4997
5181
|
_statsInterval = null;
|
|
4998
5182
|
_eventCleanup = [];
|
|
5183
|
+
_updateToolbarEmpty = () => {
|
|
5184
|
+
};
|
|
4999
5185
|
// ── Constructor ────────────────────────────────────────────────────────
|
|
5000
5186
|
constructor() {
|
|
5001
5187
|
super();
|
|
@@ -5019,12 +5205,23 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5019
5205
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
5020
5206
|
this._rippleLeft = shadow.querySelector(".avp-ripple-left");
|
|
5021
5207
|
this._rippleRight = shadow.querySelector(".avp-ripple-right");
|
|
5208
|
+
const slots = shadow.querySelectorAll('slot[name="top-left"], slot[name="top-right"]');
|
|
5209
|
+
this._updateToolbarEmpty = () => {
|
|
5210
|
+
const hasContent = Array.from(slots).some((s) => s.assignedNodes({ flatten: true }).length > 0);
|
|
5211
|
+
if (hasContent) this.removeAttribute("data-toolbar-empty");
|
|
5212
|
+
else this.setAttribute("data-toolbar-empty", "");
|
|
5213
|
+
};
|
|
5214
|
+
for (const s of slots) s.addEventListener("slotchange", this._updateToolbarEmpty);
|
|
5022
5215
|
this._bindEvents();
|
|
5023
5216
|
}
|
|
5024
5217
|
_template() {
|
|
5025
5218
|
return `
|
|
5026
5219
|
<div part="container" class="avp">
|
|
5027
5220
|
<avbridge-video part="video"></avbridge-video>
|
|
5221
|
+
<div part="toolbar-top" class="avp-toolbar-top">
|
|
5222
|
+
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5223
|
+
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5224
|
+
</div>
|
|
5028
5225
|
<div part="overlay" class="avp-overlay">
|
|
5029
5226
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5030
5227
|
<div class="avp-spinner"></div>
|
|
@@ -5177,13 +5374,14 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5177
5374
|
});
|
|
5178
5375
|
});
|
|
5179
5376
|
on(this, "keydown", (e) => this._onKeydown(e));
|
|
5180
|
-
if (!this.hasAttribute("tabindex")) {
|
|
5181
|
-
this.setAttribute("tabindex", "0");
|
|
5182
|
-
}
|
|
5183
5377
|
}
|
|
5184
5378
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
5185
5379
|
connectedCallback() {
|
|
5186
5380
|
this._setState("idle");
|
|
5381
|
+
if (!this.hasAttribute("tabindex")) {
|
|
5382
|
+
this.setAttribute("tabindex", "0");
|
|
5383
|
+
}
|
|
5384
|
+
this._updateToolbarEmpty();
|
|
5187
5385
|
}
|
|
5188
5386
|
disconnectedCallback() {
|
|
5189
5387
|
this._clearTimers();
|
|
@@ -5445,8 +5643,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5445
5643
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5446
5644
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5447
5645
|
_lastPointerTypeWasTouch = false;
|
|
5646
|
+
/** True if the event's composed path passes through consumer-slotted toolbar
|
|
5647
|
+
* content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
|
|
5648
|
+
* on the event target won't find the shadow-DOM wrapper — `composedPath()`
|
|
5649
|
+
* does. */
|
|
5650
|
+
_isToolbarEvent(e) {
|
|
5651
|
+
for (const node of e.composedPath()) {
|
|
5652
|
+
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
5653
|
+
}
|
|
5654
|
+
return false;
|
|
5655
|
+
}
|
|
5448
5656
|
_onContainerClick(e) {
|
|
5449
5657
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5658
|
+
if (this._isToolbarEvent(e)) return;
|
|
5450
5659
|
if (this._lastPointerTypeWasTouch) {
|
|
5451
5660
|
this._lastPointerTypeWasTouch = false;
|
|
5452
5661
|
return;
|
|
@@ -5462,6 +5671,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5462
5671
|
}
|
|
5463
5672
|
_onContainerDblClick(e) {
|
|
5464
5673
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5674
|
+
if (this._isToolbarEvent(e)) return;
|
|
5465
5675
|
if (this._tapTimer) {
|
|
5466
5676
|
clearTimeout(this._tapTimer);
|
|
5467
5677
|
this._tapTimer = null;
|
|
@@ -5483,6 +5693,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5483
5693
|
if (e.pointerType !== "touch") return;
|
|
5484
5694
|
this._lastPointerTypeWasTouch = true;
|
|
5485
5695
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5696
|
+
if (this._isToolbarEvent(e)) return;
|
|
5486
5697
|
const now = Date.now();
|
|
5487
5698
|
if (now - this._lastTapTime < 300) {
|
|
5488
5699
|
if (this._tapTimer) {
|