movi-player 0.3.4 → 0.3.5
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/dist/core/MoviPlayer.d.ts.map +1 -1
- package/dist/core/MoviPlayer.js +7 -1
- package/dist/core/MoviPlayer.js.map +1 -1
- package/dist/element.cjs +89 -72
- package/dist/element.js +89 -72
- package/dist/index.cjs +89 -72
- package/dist/index.js +89 -72
- package/dist/player.cjs +50 -48
- package/dist/player.js +50 -48
- package/dist/render/CanvasRenderer.d.ts.map +1 -1
- package/dist/render/CanvasRenderer.js +50 -25
- package/dist/render/CanvasRenderer.js.map +1 -1
- package/dist/render/MoviElement.d.ts +14 -0
- package/dist/render/MoviElement.d.ts.map +1 -1
- package/dist/render/MoviElement.js +82 -23
- package/dist/render/MoviElement.js.map +1 -1
- package/package.json +1 -1
|
@@ -75,6 +75,10 @@ export class MoviElement extends HTMLElement {
|
|
|
75
75
|
// Blurred-backdrop element behind the sharp-art canvas — CSS filter:blur()
|
|
76
76
|
// (cross-browser Gaussian, unlike canvas ctx.filter).
|
|
77
77
|
_coverArtBgEl = null;
|
|
78
|
+
// A small data URL of the embedded album art, generated once per source, so
|
|
79
|
+
// the CSS-blurred backdrop can point at it too (background-image needs a URL;
|
|
80
|
+
// the embedded art arrives as an ImageBitmap with none).
|
|
81
|
+
_coverArtBgUrl = "";
|
|
78
82
|
// True once cover-art extraction has settled for the current source (bitmap
|
|
79
83
|
// arrived OR extraction failed). Until then, if the source has an art track,
|
|
80
84
|
// we hold the audio-strip layout off so the player doesn't flash strip→cover.
|
|
@@ -3432,7 +3436,7 @@ export class MoviElement extends HTMLElement {
|
|
|
3432
3436
|
if (this.player && !this._pipWindow) {
|
|
3433
3437
|
const deg = this.player.rotateVideo();
|
|
3434
3438
|
this.showOSD(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>`, `${deg}°`);
|
|
3435
|
-
const statusEl = this.
|
|
3439
|
+
const statusEl = this.contextMenuRoot().querySelector(".movi-rotate-status");
|
|
3436
3440
|
if (statusEl)
|
|
3437
3441
|
statusEl.textContent = `${deg}°`;
|
|
3438
3442
|
this.syncThumbnailRotation(deg);
|
|
@@ -3852,7 +3856,16 @@ export class MoviElement extends HTMLElement {
|
|
|
3852
3856
|
// useless. Switch to position:fixed in viewport coordinates so
|
|
3853
3857
|
// the menu can spill into the empty page area below the strip.
|
|
3854
3858
|
const isStripMode = this.classList.contains("movi-audio-strip");
|
|
3855
|
-
|
|
3859
|
+
// In fullscreen the player host IS the fullscreen element (the browser's
|
|
3860
|
+
// top layer), so the body-level portal renders OUTSIDE it and the menu
|
|
3861
|
+
// never appears. There are also no page clipping ancestors to escape here
|
|
3862
|
+
// (the host fills the screen). So keep the menu in the shadow root, lift
|
|
3863
|
+
// the host's paint clip so it isn't chopped at the host edge, and position
|
|
3864
|
+
// it with fixed viewport coordinates — the fullscreen host box == viewport.
|
|
3865
|
+
const inFullscreen = this.isFullscreenActive();
|
|
3866
|
+
if (isStripMode || inFullscreen) {
|
|
3867
|
+
if (inFullscreen)
|
|
3868
|
+
this.classList.add("movi-menu-overflow");
|
|
3856
3869
|
contextMenu.style.position = "fixed";
|
|
3857
3870
|
contextMenu.style.maxHeight = `${Math.max(180, window.innerHeight - 40)}px`;
|
|
3858
3871
|
contextMenu.style.display = "block";
|
|
@@ -4339,7 +4352,7 @@ export class MoviElement extends HTMLElement {
|
|
|
4339
4352
|
else if (action === "rotate-video") {
|
|
4340
4353
|
if (this.player && !this._pipWindow) {
|
|
4341
4354
|
const deg = this.player.rotateVideo();
|
|
4342
|
-
const statusEl =
|
|
4355
|
+
const statusEl = this.contextMenuRoot().querySelector(".movi-rotate-status");
|
|
4343
4356
|
if (statusEl)
|
|
4344
4357
|
statusEl.textContent = `${deg}°`;
|
|
4345
4358
|
this.syncThumbnailRotation(deg);
|
|
@@ -12014,7 +12027,7 @@ export class MoviElement extends HTMLElement {
|
|
|
12014
12027
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
12015
12028
|
border-radius: 16px;
|
|
12016
12029
|
padding: 8px 4px;
|
|
12017
|
-
min-width:
|
|
12030
|
+
min-width: 230px;
|
|
12018
12031
|
visibility: hidden;
|
|
12019
12032
|
opacity: 0;
|
|
12020
12033
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
|
@@ -12044,7 +12057,7 @@ export class MoviElement extends HTMLElement {
|
|
|
12044
12057
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
12045
12058
|
border-radius: 16px;
|
|
12046
12059
|
padding: 8px 4px;
|
|
12047
|
-
min-width:
|
|
12060
|
+
min-width: 230px;
|
|
12048
12061
|
visibility: hidden;
|
|
12049
12062
|
opacity: 0;
|
|
12050
12063
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
|
@@ -14366,6 +14379,28 @@ export class MoviElement extends HTMLElement {
|
|
|
14366
14379
|
};
|
|
14367
14380
|
img.src = url;
|
|
14368
14381
|
}
|
|
14382
|
+
/** Downscale an album-art bitmap to a tiny JPEG data URL for the blurred
|
|
14383
|
+
* backdrop. The backdrop is blurred to 40px, so ~96px is indistinguishable
|
|
14384
|
+
* from full-res while keeping the URL small and the draw cheap. */
|
|
14385
|
+
makeCoverArtBgUrl(bitmap) {
|
|
14386
|
+
try {
|
|
14387
|
+
const max = 96;
|
|
14388
|
+
const ar = bitmap.width / bitmap.height;
|
|
14389
|
+
const w = Math.max(1, ar >= 1 ? max : Math.round(max * ar));
|
|
14390
|
+
const h = Math.max(1, ar >= 1 ? Math.round(max / ar) : max);
|
|
14391
|
+
const c = document.createElement("canvas");
|
|
14392
|
+
c.width = w;
|
|
14393
|
+
c.height = h;
|
|
14394
|
+
const cx = c.getContext("2d");
|
|
14395
|
+
if (!cx)
|
|
14396
|
+
return "";
|
|
14397
|
+
cx.drawImage(bitmap, 0, 0, w, h);
|
|
14398
|
+
return c.toDataURL("image/jpeg", 0.7);
|
|
14399
|
+
}
|
|
14400
|
+
catch {
|
|
14401
|
+
return "";
|
|
14402
|
+
}
|
|
14403
|
+
}
|
|
14369
14404
|
updateCoverArtOverlay() {
|
|
14370
14405
|
const overlay = this.coverArtOverlay;
|
|
14371
14406
|
const canvas = this.coverArtCanvas;
|
|
@@ -14497,11 +14532,12 @@ export class MoviElement extends HTMLElement {
|
|
|
14497
14532
|
ctx.scale(dpr, dpr);
|
|
14498
14533
|
// Blurred backdrop is now a CSS-blurred DOM element BEHIND this canvas — a
|
|
14499
14534
|
// real Gaussian in every browser (canvas ctx.filter "blur()" is unsupported
|
|
14500
|
-
// in Safari < 17 and looked bad faked). Point it at the poster URL
|
|
14501
|
-
// art (an ImageBitmap
|
|
14502
|
-
//
|
|
14535
|
+
// in Safari < 17 and looked bad faked). Point it at the poster URL, or at a
|
|
14536
|
+
// baked-down data URL of the embedded album art (an ImageBitmap has no URL
|
|
14537
|
+
// of its own), so both cover-art sources get the same blurred surround. The
|
|
14538
|
+
// canvas paints only the sharp art.
|
|
14503
14539
|
if (this._coverArtBgEl) {
|
|
14504
|
-
const bgUrl = this.coverArtBitmap ?
|
|
14540
|
+
const bgUrl = this.coverArtBitmap ? this._coverArtBgUrl : this._posterCoverUrl;
|
|
14505
14541
|
this._coverArtBgEl.style.backgroundImage = bgUrl
|
|
14506
14542
|
? `url("${bgUrl.replace(/"/g, '\\"')}")`
|
|
14507
14543
|
: "none";
|
|
@@ -15495,6 +15531,10 @@ export class MoviElement extends HTMLElement {
|
|
|
15495
15531
|
// and reuses one on subsequent loads. Our reference can be dropped
|
|
15496
15532
|
// safely; the player will close() when it stomps its own slot.
|
|
15497
15533
|
this.coverArtBitmap = bitmap;
|
|
15534
|
+
// Bake a tiny data URL from the art so the CSS-blurred backdrop can use
|
|
15535
|
+
// it (the poster path already had a URL; embedded art didn't). Generated
|
|
15536
|
+
// once here, not on every resize.
|
|
15537
|
+
this._coverArtBgUrl = bitmap ? this.makeCoverArtBgUrl(bitmap) : "";
|
|
15498
15538
|
// Extraction has settled (success or failure) — release the strip-layout
|
|
15499
15539
|
// hold so a failed extraction falls back to the strip cleanly.
|
|
15500
15540
|
this._coverArtResolved = true;
|
|
@@ -15528,6 +15568,7 @@ export class MoviElement extends HTMLElement {
|
|
|
15528
15568
|
// owned by MoviPlayer; we just clear our own pointer and hide the
|
|
15529
15569
|
// overlay so the new load doesn't briefly show last track's art.
|
|
15530
15570
|
this.coverArtBitmap = null;
|
|
15571
|
+
this._coverArtBgUrl = "";
|
|
15531
15572
|
this._coverArtResolved = false;
|
|
15532
15573
|
// Drop the poster-as-album-art bitmap too — we own it, so close to free it.
|
|
15533
15574
|
if (this._posterCoverBitmap) {
|
|
@@ -17441,16 +17482,31 @@ export class MoviElement extends HTMLElement {
|
|
|
17441
17482
|
}
|
|
17442
17483
|
this.updateLoopUI();
|
|
17443
17484
|
}
|
|
17485
|
+
/**
|
|
17486
|
+
* The root the context menu currently lives in. On desktop the menu portals
|
|
17487
|
+
* to a body-level shadow root while open, so its items are NOT in this.shadowRoot
|
|
17488
|
+
* at that moment — a toggle-state updater querying the shadow root would find
|
|
17489
|
+
* nothing and silently no-op (the reason menu toggles didn't reflect their new
|
|
17490
|
+
* state after a click). Query menu items through this so they're found whether
|
|
17491
|
+
* the menu is portaled or still home in the shadow root.
|
|
17492
|
+
*/
|
|
17493
|
+
contextMenuRoot() {
|
|
17494
|
+
if (this._menuPortalRoot?.querySelector(".movi-context-menu")) {
|
|
17495
|
+
return this._menuPortalRoot;
|
|
17496
|
+
}
|
|
17497
|
+
return this.shadowRoot;
|
|
17498
|
+
}
|
|
17444
17499
|
updateLoopUI() {
|
|
17445
17500
|
const shadowRoot = this.shadowRoot;
|
|
17446
17501
|
if (!shadowRoot)
|
|
17447
17502
|
return;
|
|
17503
|
+
const menuRoot = this.contextMenuRoot();
|
|
17448
17504
|
const loopBtn = shadowRoot.querySelector(".movi-loop-btn");
|
|
17449
|
-
const loopMenuItem =
|
|
17450
|
-
const loopStatus =
|
|
17505
|
+
const loopMenuItem = menuRoot.querySelector('.movi-context-menu-item[data-action="loop-toggle"]');
|
|
17506
|
+
const loopStatus = menuRoot.querySelector(".movi-loop-status");
|
|
17451
17507
|
// Context menu icons
|
|
17452
|
-
const ctxOutline =
|
|
17453
|
-
const ctxFilled =
|
|
17508
|
+
const ctxOutline = menuRoot.querySelector(".movi-context-menu-loop-outline");
|
|
17509
|
+
const ctxFilled = menuRoot.querySelector(".movi-context-menu-loop-filled");
|
|
17454
17510
|
if (this._loop) {
|
|
17455
17511
|
loopBtn?.classList.add("active");
|
|
17456
17512
|
loopMenuItem?.classList.add("movi-context-menu-active");
|
|
@@ -17476,10 +17532,11 @@ export class MoviElement extends HTMLElement {
|
|
|
17476
17532
|
const shadowRoot = this.shadowRoot;
|
|
17477
17533
|
if (!shadowRoot)
|
|
17478
17534
|
return;
|
|
17479
|
-
const
|
|
17480
|
-
const
|
|
17481
|
-
const
|
|
17482
|
-
const
|
|
17535
|
+
const menuRoot = this.contextMenuRoot();
|
|
17536
|
+
const menuItem = menuRoot.querySelector('.movi-context-menu-item[data-action="ambient-toggle"]');
|
|
17537
|
+
const status = menuRoot.querySelector(".movi-ambient-status");
|
|
17538
|
+
const ctxOutline = menuRoot.querySelector(".movi-context-menu-ambient-outline");
|
|
17539
|
+
const ctxFilled = menuRoot.querySelector(".movi-context-menu-ambient-filled");
|
|
17483
17540
|
if (this._ambientMode) {
|
|
17484
17541
|
menuItem?.classList.add("movi-context-menu-active");
|
|
17485
17542
|
if (status)
|
|
@@ -17503,12 +17560,13 @@ export class MoviElement extends HTMLElement {
|
|
|
17503
17560
|
if (!shadowRoot)
|
|
17504
17561
|
return;
|
|
17505
17562
|
const isEnabled = this.player?.getStableAudio() ?? true;
|
|
17563
|
+
const menuRoot = this.contextMenuRoot();
|
|
17506
17564
|
const stableBtn = shadowRoot.querySelector(".movi-stable-audio-btn");
|
|
17507
|
-
const stableMenuItem =
|
|
17508
|
-
const stableStatus =
|
|
17565
|
+
const stableMenuItem = menuRoot.querySelector('.movi-context-menu-item[data-action="stable-audio-toggle"]');
|
|
17566
|
+
const stableStatus = menuRoot.querySelector(".movi-stable-audio-status");
|
|
17509
17567
|
// Context menu icons
|
|
17510
|
-
const ctxOutline =
|
|
17511
|
-
const ctxFilled =
|
|
17568
|
+
const ctxOutline = menuRoot.querySelector(".movi-context-menu-stable-outline");
|
|
17569
|
+
const ctxFilled = menuRoot.querySelector(".movi-context-menu-stable-filled");
|
|
17512
17570
|
if (isEnabled) {
|
|
17513
17571
|
stableBtn?.classList.add("active");
|
|
17514
17572
|
stableMenuItem?.classList.add("movi-context-menu-active");
|
|
@@ -18977,9 +19035,10 @@ export class MoviElement extends HTMLElement {
|
|
|
18977
19035
|
return !!this.player?.isHDRSupported?.();
|
|
18978
19036
|
}
|
|
18979
19037
|
updateHDRUI() {
|
|
19038
|
+
const menuRoot = this.contextMenuRoot();
|
|
18980
19039
|
const hdrBtn = this.shadowRoot?.querySelector(".movi-hdr-btn");
|
|
18981
|
-
const hdrStatus =
|
|
18982
|
-
const hdrMenuItem =
|
|
19040
|
+
const hdrStatus = menuRoot.querySelector(".movi-hdr-status");
|
|
19041
|
+
const hdrMenuItem = menuRoot.querySelector('.movi-context-menu-item[data-action="hdr-toggle"]');
|
|
18983
19042
|
if (this._hdr) {
|
|
18984
19043
|
hdrBtn?.classList.add("movi-hdr-active");
|
|
18985
19044
|
hdrMenuItem?.classList.add("movi-context-menu-active");
|