avbridge 2.7.0 → 2.8.2
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 +106 -0
- package/README.md +35 -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 +254 -12
- package/dist/player.cjs.map +1 -1
- package/dist/player.d.cts +62 -3
- package/dist/player.d.ts +62 -3
- package/dist/player.js +254 -12
- package/dist/player.js.map +1 -1
- package/package.json +1 -1
- package/src/classify/rules.ts +23 -0
- package/src/element/avbridge-player.ts +83 -7
- 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,10 +5141,13 @@ var PROXY_ATTRIBUTES = [
|
|
|
4958
5141
|
"playsinline",
|
|
4959
5142
|
"crossorigin",
|
|
4960
5143
|
"disableremoteplayback",
|
|
4961
|
-
"preferstrategy"
|
|
5144
|
+
"preferstrategy",
|
|
5145
|
+
"fit"
|
|
4962
5146
|
];
|
|
5147
|
+
var PLAYER_ATTRIBUTES = ["show-fit"];
|
|
5148
|
+
var FIT_MODES = ["contain", "cover", "fill"];
|
|
4963
5149
|
var AvbridgePlayerElement = class extends HTMLElement {
|
|
4964
|
-
static observedAttributes = [...PROXY_ATTRIBUTES];
|
|
5150
|
+
static observedAttributes = [...PROXY_ATTRIBUTES, ...PLAYER_ATTRIBUTES];
|
|
4965
5151
|
// ── Internal DOM refs ──────────────────────────────────────────────────
|
|
4966
5152
|
_video;
|
|
4967
5153
|
_playBtn;
|
|
@@ -4996,6 +5182,9 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
4996
5182
|
_statsEl;
|
|
4997
5183
|
_statsInterval = null;
|
|
4998
5184
|
_eventCleanup = [];
|
|
5185
|
+
_updateToolbarEmpty = () => {
|
|
5186
|
+
};
|
|
5187
|
+
_toolbarTop;
|
|
4999
5188
|
// ── Constructor ────────────────────────────────────────────────────────
|
|
5000
5189
|
constructor() {
|
|
5001
5190
|
super();
|
|
@@ -5019,12 +5208,25 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5019
5208
|
this._statsEl = shadow.querySelector(".avp-stats");
|
|
5020
5209
|
this._rippleLeft = shadow.querySelector(".avp-ripple-left");
|
|
5021
5210
|
this._rippleRight = shadow.querySelector(".avp-ripple-right");
|
|
5211
|
+
this._toolbarTop = shadow.querySelector('[part="toolbar-top"]');
|
|
5212
|
+
this._toolbarTop.setAttribute("data-visible", "true");
|
|
5213
|
+
const slots = shadow.querySelectorAll('slot[name="top-left"], slot[name="top-right"]');
|
|
5214
|
+
this._updateToolbarEmpty = () => {
|
|
5215
|
+
const hasContent = Array.from(slots).some((s) => s.assignedNodes({ flatten: true }).length > 0);
|
|
5216
|
+
if (hasContent) this.removeAttribute("data-toolbar-empty");
|
|
5217
|
+
else this.setAttribute("data-toolbar-empty", "");
|
|
5218
|
+
};
|
|
5219
|
+
for (const s of slots) s.addEventListener("slotchange", this._updateToolbarEmpty);
|
|
5022
5220
|
this._bindEvents();
|
|
5023
5221
|
}
|
|
5024
5222
|
_template() {
|
|
5025
5223
|
return `
|
|
5026
5224
|
<div part="container" class="avp">
|
|
5027
5225
|
<avbridge-video part="video"></avbridge-video>
|
|
5226
|
+
<div part="toolbar-top" class="avp-toolbar-top">
|
|
5227
|
+
<div part="toolbar-top-left" class="avp-toolbar-top-left"><slot name="top-left"></slot></div>
|
|
5228
|
+
<div part="toolbar-top-right" class="avp-toolbar-top-right"><slot name="top-right"></slot></div>
|
|
5229
|
+
</div>
|
|
5028
5230
|
<div part="overlay" class="avp-overlay">
|
|
5029
5231
|
<button class="avp-overlay-btn" aria-label="Play">${ICON_PLAY}</button>
|
|
5030
5232
|
<div class="avp-spinner"></div>
|
|
@@ -5177,19 +5379,26 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5177
5379
|
});
|
|
5178
5380
|
});
|
|
5179
5381
|
on(this, "keydown", (e) => this._onKeydown(e));
|
|
5180
|
-
if (!this.hasAttribute("tabindex")) {
|
|
5181
|
-
this.setAttribute("tabindex", "0");
|
|
5182
|
-
}
|
|
5183
5382
|
}
|
|
5184
5383
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
5185
5384
|
connectedCallback() {
|
|
5186
5385
|
this._setState("idle");
|
|
5386
|
+
if (!this.hasAttribute("tabindex")) {
|
|
5387
|
+
this.setAttribute("tabindex", "0");
|
|
5388
|
+
}
|
|
5389
|
+
this._updateToolbarEmpty();
|
|
5187
5390
|
}
|
|
5188
5391
|
disconnectedCallback() {
|
|
5189
5392
|
this._clearTimers();
|
|
5190
5393
|
}
|
|
5191
5394
|
attributeChangedCallback(name, _old, value) {
|
|
5192
5395
|
if (!this._video) return;
|
|
5396
|
+
if (PLAYER_ATTRIBUTES.includes(name)) {
|
|
5397
|
+
if (name === "show-fit" && this._settingsOpen) {
|
|
5398
|
+
this._buildSettingsMenu();
|
|
5399
|
+
}
|
|
5400
|
+
return;
|
|
5401
|
+
}
|
|
5193
5402
|
if (value == null) this._video.removeAttribute(name);
|
|
5194
5403
|
else this._video.setAttribute(name, value);
|
|
5195
5404
|
}
|
|
@@ -5312,6 +5521,16 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5312
5521
|
}
|
|
5313
5522
|
_buildSettingsMenu() {
|
|
5314
5523
|
const sections = [];
|
|
5524
|
+
if (this.hasAttribute("show-fit")) {
|
|
5525
|
+
const currentFit = this._video.fit ?? "contain";
|
|
5526
|
+
let fitItems = "";
|
|
5527
|
+
for (const mode of FIT_MODES) {
|
|
5528
|
+
const active = mode === currentFit;
|
|
5529
|
+
const label = mode[0].toUpperCase() + mode.slice(1);
|
|
5530
|
+
fitItems += `<div class="avp-settings-item${active ? " active" : ""}" data-fit="${mode}">${label}</div>`;
|
|
5531
|
+
}
|
|
5532
|
+
sections.push(`<div class="avp-settings-section"><div class="avp-settings-label">Fit</div>${fitItems}</div>`);
|
|
5533
|
+
}
|
|
5315
5534
|
const currentRate = this._video.playbackRate ?? 1;
|
|
5316
5535
|
let speedItems = "";
|
|
5317
5536
|
for (const spd of PLAYBACK_SPEEDS) {
|
|
@@ -5338,6 +5557,14 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5338
5557
|
}
|
|
5339
5558
|
sections.push(`<div class="avp-settings-section"><div class="avp-settings-item" data-stats>Stats for nerds</div></div>`);
|
|
5340
5559
|
this._settingsMenu.innerHTML = sections.join("");
|
|
5560
|
+
for (const item of this._settingsMenu.querySelectorAll("[data-fit]")) {
|
|
5561
|
+
item.addEventListener("click", (e) => {
|
|
5562
|
+
e.stopPropagation();
|
|
5563
|
+
const mode = item.dataset.fit;
|
|
5564
|
+
this.setAttribute("fit", mode);
|
|
5565
|
+
this._buildSettingsMenu();
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5341
5568
|
for (const item of this._settingsMenu.querySelectorAll("[data-speed]")) {
|
|
5342
5569
|
item.addEventListener("click", (e) => {
|
|
5343
5570
|
e.stopPropagation();
|
|
@@ -5423,6 +5650,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5423
5650
|
// ── Controls: auto-hide ────────────────────────────────────────────────
|
|
5424
5651
|
_showControls() {
|
|
5425
5652
|
this.removeAttribute("data-controls-hidden");
|
|
5653
|
+
this._toolbarTop.setAttribute("data-visible", "true");
|
|
5426
5654
|
this._scheduleHide();
|
|
5427
5655
|
}
|
|
5428
5656
|
_scheduleHide() {
|
|
@@ -5432,6 +5660,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5432
5660
|
this._controlsTimer = setTimeout(() => {
|
|
5433
5661
|
if (this._state === "playing") {
|
|
5434
5662
|
this.setAttribute("data-controls-hidden", "");
|
|
5663
|
+
this._toolbarTop.setAttribute("data-visible", "false");
|
|
5435
5664
|
}
|
|
5436
5665
|
}, CONTROLS_HIDE_MS);
|
|
5437
5666
|
}
|
|
@@ -5445,8 +5674,19 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5445
5674
|
// it's treated as a double-click and the single-click action is cancelled.
|
|
5446
5675
|
/** Track whether the last interaction was touch so click handler can skip. */
|
|
5447
5676
|
_lastPointerTypeWasTouch = false;
|
|
5677
|
+
/** True if the event's composed path passes through consumer-slotted toolbar
|
|
5678
|
+
* content. Slotted content lives in the light DOM so `.closest(".avp-toolbar-top")`
|
|
5679
|
+
* on the event target won't find the shadow-DOM wrapper — `composedPath()`
|
|
5680
|
+
* does. */
|
|
5681
|
+
_isToolbarEvent(e) {
|
|
5682
|
+
for (const node of e.composedPath()) {
|
|
5683
|
+
if (node instanceof HTMLElement && node.classList.contains("avp-toolbar-top")) return true;
|
|
5684
|
+
}
|
|
5685
|
+
return false;
|
|
5686
|
+
}
|
|
5448
5687
|
_onContainerClick(e) {
|
|
5449
5688
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5689
|
+
if (this._isToolbarEvent(e)) return;
|
|
5450
5690
|
if (this._lastPointerTypeWasTouch) {
|
|
5451
5691
|
this._lastPointerTypeWasTouch = false;
|
|
5452
5692
|
return;
|
|
@@ -5462,6 +5702,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5462
5702
|
}
|
|
5463
5703
|
_onContainerDblClick(e) {
|
|
5464
5704
|
if (e.target.closest?.(".avp-controls, .avp-settings")) return;
|
|
5705
|
+
if (this._isToolbarEvent(e)) return;
|
|
5465
5706
|
if (this._tapTimer) {
|
|
5466
5707
|
clearTimeout(this._tapTimer);
|
|
5467
5708
|
this._tapTimer = null;
|
|
@@ -5483,6 +5724,7 @@ var AvbridgePlayerElement = class extends HTMLElement {
|
|
|
5483
5724
|
if (e.pointerType !== "touch") return;
|
|
5484
5725
|
this._lastPointerTypeWasTouch = true;
|
|
5485
5726
|
if (e.target.closest?.(".avp-controls, .avp-settings, .avp-overlay-btn")) return;
|
|
5727
|
+
if (this._isToolbarEvent(e)) return;
|
|
5486
5728
|
const now = Date.now();
|
|
5487
5729
|
if (now - this._lastTapTime < 300) {
|
|
5488
5730
|
if (this._tapTimer) {
|