movi-player 0.3.1 → 0.3.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/dist/element.cjs CHANGED
@@ -49950,7 +49950,7 @@ class MoviVideoDecoder {
49950
49950
  }
49951
49951
  async configure(A, v, b = 0) {
49952
49952
  if (this.currentTrack = A, this.targetFps = b, this.currentProfile = A.profile, this.useSoftware = false, this.requiresSoftware = false, this.isAnnexBSource = false, this.openGopErrorCount = 0, this.hardwareRetryCount = 0, this.lastHardwareRetryTime = 0, this.swDecoder && (this.swDecoder.close(), this.swDecoder = null), this.forceSoftware) return Logger$1.info(TAG$h, "Force software decoding enabled, using WASM decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49953
- if (!("VideoDecoder" in window)) return Logger$1.error(TAG$h, "WebCodecs VideoDecoder not supported"), false;
49953
+ if (!("VideoDecoder" in window)) return Logger$1.warn(TAG$h, "WebCodecs VideoDecoder not supported — falling back to software decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49954
49954
  let j = CodecParser.getCodecString(A.codec, A.extradata);
49955
49955
  if (j || (Logger$1.debug(TAG$h, "CodecParser returned null, falling back to manual mapping"), j = this.mapCodecToWebCodecs(A.codec, A.width, A.height, A.profile, A.level)), !j) return Logger$1.error(TAG$h, `Unsupported codec: ${A.codec}`), false;
49956
49956
  const s2 = { codec: j, codedWidth: A.width, codedHeight: A.height, hardwareAcceleration: "prefer-hardware" };
@@ -108886,6 +108886,7 @@ class MoviElement extends HTMLElement {
108886
108886
  isOverControls = false;
108887
108887
  isSeeking = false;
108888
108888
  pendingSeekTarget = null;
108889
+ _pendingSeek = null;
108889
108890
  isDragging = false;
108890
108891
  isTouchDragging = false;
108891
108892
  touchStartX = 0;
@@ -109011,7 +109012,7 @@ class MoviElement extends HTMLElement {
109011
109012
  return ["src", "autoplay", "controls", "loop", "muted", "playsinline", "preload", "poster", "width", "height", "crossorigin", "volume", "playbackrate", "subtitledelay", "subtitlesize", "subtitlecolor", "subtitlebg", "subtitleedge", "ambientmode", "ambientwrapper", "renderer", "objectfit", "thumb", "hdr", "theme", "sw", "fps", "gesturefs", "nohotkeys", "startat", "fastseek", "doubletap", "themecolor", "buffersize", "title", "showtitle", "resume", "stablevolume", "encrypted", "tokenurl", "videourl", "videoid", "drm", "licenseurl", "licenseheaders", "headers", "audioonly", "lcevc", "lcevcurl", "postertime"];
109012
109013
  }
109013
109014
  constructor() {
109014
- super(), this.tabIndex = 0, Logger$1.setLevel(LogLevel.DEBUG);
109015
+ super(), Logger$1.setLevel(LogLevel.DEBUG);
109015
109016
  const A = this.attachShadow({ mode: "open" });
109016
109017
  this.canvas = document.createElement("canvas"), this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.display = "block", this.canvas.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), A.appendChild(this.canvas), this.video = document.createElement("video"), this.video.style.width = "100%", this.video.style.height = "100%", this.video.style.display = "none", this.video.style.objectFit = "contain", this.video.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), A.appendChild(this.video), this.posterElement = document.createElement("img"), this.posterElement.className = "movi-poster-overlay", this.posterElement.crossOrigin = "anonymous", this.posterElement.referrerPolicy = "no-referrer", this.posterElement.decoding = "async", this.posterElement.addEventListener("error", () => {
109017
109018
  const A2 = this.posterElement.src.match(/\/vi\/([\w-]+)\/maxresdefault\.jpg/);
@@ -110878,6 +110879,7 @@ class MoviElement extends HTMLElement {
110878
110879
  const A2 = this.player?.getState();
110879
110880
  "playing" !== A2 && "buffering" !== A2 && b.classList.add("movi-center-visible");
110880
110881
  }
110882
+ this._hasEverPlayed ? this.classList.add("movi-bar-visible") : this.classList.remove("movi-bar-visible");
110881
110883
  const E = this.shadowRoot?.querySelector(".movi-controls-bar"), F = E?.offsetHeight ?? 80, k = this.shadowRoot?.querySelector(".movi-timeline-panel");
110882
110884
  let x = F + 20;
110883
110885
  if (k && "none" !== k.style.display ? (k.style.bottom = `${F + 20}px`, requestAnimationFrame(() => {
@@ -110967,7 +110969,7 @@ class MoviElement extends HTMLElement {
110967
110969
  b2 && (b2.style.cursor = "none");
110968
110970
  }
110969
110971
  const v = this.shadowRoot?.querySelector(".movi-center-play-pause"), b = this.player?.getState();
110970
- v && "playing" === b && v.classList.remove("movi-center-visible");
110972
+ v && "playing" === b && v.classList.remove("movi-center-visible"), this.classList.remove("movi-bar-visible");
110971
110973
  const j = this.shadowRoot?.querySelector(".movi-timeline-panel");
110972
110974
  if (j && (j.style.bottom = "12px"), this.player && (j && "none" !== j.style.display ? requestAnimationFrame(() => {
110973
110975
  const A2 = j.offsetHeight || 0;
@@ -111149,6 +111151,14 @@ class MoviElement extends HTMLElement {
111149
111151
  -moz-osx-font-smoothing: grayscale;
111150
111152
  }
111151
111153
 
111154
+ /* The :host{display:block} above is an author rule, so it beats the
111155
+ UA stylesheet's [hidden]{display:none}. Without this, the standard
111156
+ hidden attribute does nothing and the player stays visible. Honour
111157
+ it explicitly. !important so source-order/specificity can't regress it. */
111158
+ :host([hidden]) {
111159
+ display: none !important;
111160
+ }
111161
+
111152
111162
  /* Light Theme Override */
111153
111163
  :host([theme="light"]) {
111154
111164
  --movi-primary: #7c3aed; /* Vibrancy boost for light theme */
@@ -113737,12 +113747,13 @@ class MoviElement extends HTMLElement {
113737
113747
  height: 64px !important;
113738
113748
  }
113739
113749
 
113740
- /* Disable animations on mobile */
113750
+ /* Keyframe animations off on small/compact layouts for perf, but keep
113751
+ CSS transitions so the controls + centre button still move smoothly
113752
+ here too (the PiP / mini player is a small container). */
113741
113753
  .movi-controls-overlay,
113742
113754
  .movi-center-play-pause,
113743
113755
  .movi-btn,
113744
113756
  .movi-progress-handle {
113745
- transition: none !important;
113746
113757
  animation: none !important;
113747
113758
  transform: none !important;
113748
113759
  }
@@ -114234,7 +114245,7 @@ class MoviElement extends HTMLElement {
114234
114245
  button's bar-hidden and both-bars-visible rules exactly. */
114235
114246
  .movi-loading-indicator {
114236
114247
  position: absolute;
114237
- top: calc(50% - var(--movi-controls-height) / 2);
114248
+ top: 50%;
114238
114249
  left: 50%;
114239
114250
  transform: translate(-50%, -50%);
114240
114251
  display: flex;
@@ -114243,15 +114254,7 @@ class MoviElement extends HTMLElement {
114243
114254
  z-index: 1000;
114244
114255
  pointer-events: none;
114245
114256
  background: transparent;
114246
- }
114247
-
114248
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-loading-indicator,
114249
- :host(:not([controls])) .movi-loading-indicator {
114250
- top: 50%;
114251
- }
114252
-
114253
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-loading-indicator {
114254
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114257
+ transition: top var(--movi-transition-normal);
114255
114258
  }
114256
114259
 
114257
114260
  .movi-loader-container {
@@ -114293,7 +114296,7 @@ class MoviElement extends HTMLElement {
114293
114296
  down adjust this for the other two layout states. */
114294
114297
  .movi-center-play-pause {
114295
114298
  position: absolute;
114296
- top: calc(50% - var(--movi-controls-height) / 2);
114299
+ top: 50%;
114297
114300
  left: 50%;
114298
114301
  transform: translate(-50%, -50%) scale(0.8);
114299
114302
  z-index: 5;
@@ -114322,23 +114325,15 @@ class MoviElement extends HTMLElement {
114322
114325
  transition-delay: 0s;
114323
114326
  }
114324
114327
 
114325
- /* Bar hidden or no controls attribute no chrome to balance
114326
- against, sit at true geometric centre. */
114327
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-center-play-pause,
114328
- :host(:not([controls])) .movi-center-play-pause {
114329
- top: 50%;
114330
- }
114331
-
114332
- /* Bar visible AND title bar visible centre the icon inside
114333
- the visible video band (between title bottom and controls
114334
- top), not the full player. The 44% default biases too far up
114335
- when there's also a title bar to balance the controls; here
114336
- the offset is just half the *difference* between the two
114337
- chrome heights (title ≈ 52px, controls 72px → ~10px above
114338
- centre on desktop). calc() keeps it right on mobile where
114339
- --movi-controls-height drops to 64px too. */
114340
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-center-play-pause {
114341
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114328
+ /* Default (the base rule) is the true geometric centre. When the controls
114329
+ bar is visible, lift the centre button AND the loading spinner a little
114330
+ so they read as centred in the band above the bar. Keyed off a host
114331
+ class toggled in show/hideControls, because :host:has() checks against
114332
+ the shadow tree don't apply in every engine (notably Electron). The
114333
+ transition on top (in each base rule) animates the move. */
114334
+ :host(.movi-bar-visible) .movi-center-play-pause,
114335
+ :host(.movi-bar-visible) .movi-loading-indicator {
114336
+ top: calc(50% - var(--movi-controls-height) / 4);
114342
114337
  }
114343
114338
 
114344
114339
  .movi-center-play-pause:hover {
@@ -115937,6 +115932,7 @@ class MoviElement extends HTMLElement {
115937
115932
  });
115938
115933
  };
115939
115934
  connectedCallback() {
115935
+ this.hasAttribute("tabindex") || (this.tabIndex = 0);
115940
115936
  const A = this.getAttribute("src");
115941
115937
  this._src = A || null, this._autoplay = this.hasAttribute("autoplay"), this._controls = this.hasAttribute("controls"), this._loop = this.hasAttribute("loop"), this._muted = this.hasAttribute("muted"), this._playsinline = this.hasAttribute("playsinline"), this._preload = this.getAttribute("preload") || "auto", this._poster = this.getAttribute("poster") || "";
115942
115938
  const v = this.getAttribute("volume");
@@ -116383,7 +116379,11 @@ class MoviElement extends HTMLElement {
116383
116379
  };
116384
116380
  this.player.trackManager.on("tracksChange", b), this.eventHandlers.set("tracksChange", () => this.player?.trackManager.off("tracksChange", b));
116385
116381
  const j = (A2) => {
116386
- if (Logger$1.info(TAG, `stateChange: ${A2}`), "playing" === A2 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A2 })), this.updateLoadingIndicator(A2), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A2 && "ended" !== A2 && "error" !== A2 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A2) {
116382
+ if (Logger$1.info(TAG, `stateChange: ${A2}`), null !== this._pendingSeek && ("ready" === A2 || "paused" === A2 || "playing" === A2)) {
116383
+ const A3 = this._pendingSeek;
116384
+ this._pendingSeek = null, this.currentTime = A3;
116385
+ }
116386
+ if ("playing" === A2 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A2 })), this.updateLoadingIndicator(A2), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A2 && "ended" !== A2 && "error" !== A2 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A2) {
116387
116387
  const A3 = this._autoplayStarting;
116388
116388
  if (this._autoplayStarting = false, this._resumeDialogPending && !this._hasEverPlayed && (this._resumeDialogPending = false, this.maybeShowResumeDialog()), this._hasEverPlayed = true, this._loopRestartInFlight = false, this.dispatchEvent(new Event("play")), A3) return this.hideControls(), this.updatePlayPauseIcon(), this._ambientMode && (this._ambientSampleInterval = 200), void ("mediaSession" in navigator && this._title && (navigator.mediaSession.metadata = new MediaMetadata({ title: this._title })));
116389
116389
  window.setTimeout(() => {
@@ -117161,10 +117161,11 @@ class MoviElement extends HTMLElement {
117161
117161
  return this.player?.getCurrentTime() || 0;
117162
117162
  }
117163
117163
  set currentTime(A) {
117164
- if (!this.player) return;
117164
+ if (!this.player) return void (this._pendingSeek = A);
117165
117165
  A = this.clampToBufferedWindow(A);
117166
117166
  const v = this.player.getState();
117167
- Logger$1.info(TAG, `currentTime setter: value=${A.toFixed(2)}, state=${v}, isSeeking=${this.isSeeking}`), "ready" === v || "playing" === v || "paused" === v || "ended" === v || "seeking" === v || "buffering" === v ? this.isSeeking ? this.pendingSeekTarget = A : (this.isSeeking = true, this.player.seek(A).then(() => {
117167
+ if (Logger$1.info(TAG, `currentTime setter: value=${A.toFixed(2)}, state=${v}, isSeeking=${this.isSeeking}`), "ready" !== v && "playing" !== v && "paused" !== v && "ended" !== v && "seeking" !== v && "buffering" !== v) return Logger$1.info(TAG, `Seek held until ready state=${v}`), void (this._pendingSeek = A);
117168
+ this._pendingSeek = null, this.isSeeking ? this.pendingSeekTarget = A : (this.isSeeking = true, this.player.seek(A).then(() => {
117168
117169
  Logger$1.info(TAG, `Seek resolved: value=${A.toFixed(2)}, newState=${this.player?.getState()}`);
117169
117170
  }).catch((v2) => {
117170
117171
  Logger$1.error(TAG, `Seek error: value=${A.toFixed(2)}`, v2);
@@ -117173,7 +117174,7 @@ class MoviElement extends HTMLElement {
117173
117174
  const A2 = this.pendingSeekTarget;
117174
117175
  this.pendingSeekTarget = null, this.currentTime = A2;
117175
117176
  }
117176
- })) : Logger$1.warn(TAG, `Seek blocked — state=${v} not allowed`);
117177
+ }));
117177
117178
  }
117178
117179
  get renderer() {
117179
117180
  return this._renderer;
package/dist/element.js CHANGED
@@ -49965,7 +49965,7 @@ class MoviVideoDecoder {
49965
49965
  }
49966
49966
  async configure(A2, t2, n2 = 0) {
49967
49967
  if (this.currentTrack = A2, this.targetFps = n2, this.currentProfile = A2.profile, this.useSoftware = false, this.requiresSoftware = false, this.isAnnexBSource = false, this.openGopErrorCount = 0, this.hardwareRetryCount = 0, this.lastHardwareRetryTime = 0, this.swDecoder && (this.swDecoder.close(), this.swDecoder = null), this.forceSoftware) return v.info(K, "Force software decoding enabled, using WASM decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49968
- if (!("VideoDecoder" in window)) return v.error(K, "WebCodecs VideoDecoder not supported"), false;
49968
+ if (!("VideoDecoder" in window)) return v.warn(K, "WebCodecs VideoDecoder not supported — falling back to software decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49969
49969
  let b2 = CodecParser.getCodecString(A2.codec, A2.extradata);
49970
49970
  if (b2 || (v.debug(K, "CodecParser returned null, falling back to manual mapping"), b2 = this.mapCodecToWebCodecs(A2.codec, A2.width, A2.height, A2.profile, A2.level)), !b2) return v.error(K, `Unsupported codec: ${A2.codec}`), false;
49971
49971
  const j2 = { codec: b2, codedWidth: A2.width, codedHeight: A2.height, hardwareAcceleration: "prefer-hardware" };
@@ -108990,6 +108990,7 @@ class MoviElement extends HTMLElement {
108990
108990
  isOverControls = false;
108991
108991
  isSeeking = false;
108992
108992
  pendingSeekTarget = null;
108993
+ _pendingSeek = null;
108993
108994
  isDragging = false;
108994
108995
  isTouchDragging = false;
108995
108996
  touchStartX = 0;
@@ -109115,7 +109116,7 @@ class MoviElement extends HTMLElement {
109115
109116
  return ["src", "autoplay", "controls", "loop", "muted", "playsinline", "preload", "poster", "width", "height", "crossorigin", "volume", "playbackrate", "subtitledelay", "subtitlesize", "subtitlecolor", "subtitlebg", "subtitleedge", "ambientmode", "ambientwrapper", "renderer", "objectfit", "thumb", "hdr", "theme", "sw", "fps", "gesturefs", "nohotkeys", "startat", "fastseek", "doubletap", "themecolor", "buffersize", "title", "showtitle", "resume", "stablevolume", "encrypted", "tokenurl", "videourl", "videoid", "drm", "licenseurl", "licenseheaders", "headers", "audioonly", "lcevc", "lcevcurl", "postertime"];
109116
109117
  }
109117
109118
  constructor() {
109118
- super(), this.tabIndex = 0, v.setLevel(A.DEBUG);
109119
+ super(), v.setLevel(A.DEBUG);
109119
109120
  const t2 = this.attachShadow({ mode: "open" });
109120
109121
  this.canvas = document.createElement("canvas"), this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.display = "block", this.canvas.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), t2.appendChild(this.canvas), this.video = document.createElement("video"), this.video.style.width = "100%", this.video.style.height = "100%", this.video.style.display = "none", this.video.style.objectFit = "contain", this.video.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), t2.appendChild(this.video), this.posterElement = document.createElement("img"), this.posterElement.className = "movi-poster-overlay", this.posterElement.crossOrigin = "anonymous", this.posterElement.referrerPolicy = "no-referrer", this.posterElement.decoding = "async", this.posterElement.addEventListener("error", () => {
109121
109122
  const A2 = this.posterElement.src.match(/\/vi\/([\w-]+)\/maxresdefault\.jpg/);
@@ -110982,6 +110983,7 @@ class MoviElement extends HTMLElement {
110982
110983
  const A3 = this.player?.getState();
110983
110984
  "playing" !== A3 && "buffering" !== A3 && v2.classList.add("movi-center-visible");
110984
110985
  }
110986
+ this._hasEverPlayed ? this.classList.add("movi-bar-visible") : this.classList.remove("movi-bar-visible");
110985
110987
  const j2 = this.shadowRoot?.querySelector(".movi-controls-bar"), s2 = j2?.offsetHeight ?? 80, f2 = this.shadowRoot?.querySelector(".movi-timeline-panel");
110986
110988
  let l2 = s2 + 20;
110987
110989
  if (f2 && "none" !== f2.style.display ? (f2.style.bottom = `${s2 + 20}px`, requestAnimationFrame(() => {
@@ -111071,7 +111073,7 @@ class MoviElement extends HTMLElement {
111071
111073
  v3 && (v3.style.cursor = "none");
111072
111074
  }
111073
111075
  const t2 = this.shadowRoot?.querySelector(".movi-center-play-pause"), v2 = this.player?.getState();
111074
- t2 && "playing" === v2 && t2.classList.remove("movi-center-visible");
111076
+ t2 && "playing" === v2 && t2.classList.remove("movi-center-visible"), this.classList.remove("movi-bar-visible");
111075
111077
  const n2 = this.shadowRoot?.querySelector(".movi-timeline-panel");
111076
111078
  if (n2 && (n2.style.bottom = "12px"), this.player && (n2 && "none" !== n2.style.display ? requestAnimationFrame(() => {
111077
111079
  const A3 = n2.offsetHeight || 0;
@@ -111253,6 +111255,14 @@ class MoviElement extends HTMLElement {
111253
111255
  -moz-osx-font-smoothing: grayscale;
111254
111256
  }
111255
111257
 
111258
+ /* The :host{display:block} above is an author rule, so it beats the
111259
+ UA stylesheet's [hidden]{display:none}. Without this, the standard
111260
+ hidden attribute does nothing and the player stays visible. Honour
111261
+ it explicitly. !important so source-order/specificity can't regress it. */
111262
+ :host([hidden]) {
111263
+ display: none !important;
111264
+ }
111265
+
111256
111266
  /* Light Theme Override */
111257
111267
  :host([theme="light"]) {
111258
111268
  --movi-primary: #7c3aed; /* Vibrancy boost for light theme */
@@ -113841,12 +113851,13 @@ class MoviElement extends HTMLElement {
113841
113851
  height: 64px !important;
113842
113852
  }
113843
113853
 
113844
- /* Disable animations on mobile */
113854
+ /* Keyframe animations off on small/compact layouts for perf, but keep
113855
+ CSS transitions so the controls + centre button still move smoothly
113856
+ here too (the PiP / mini player is a small container). */
113845
113857
  .movi-controls-overlay,
113846
113858
  .movi-center-play-pause,
113847
113859
  .movi-btn,
113848
113860
  .movi-progress-handle {
113849
- transition: none !important;
113850
113861
  animation: none !important;
113851
113862
  transform: none !important;
113852
113863
  }
@@ -114338,7 +114349,7 @@ class MoviElement extends HTMLElement {
114338
114349
  button's bar-hidden and both-bars-visible rules exactly. */
114339
114350
  .movi-loading-indicator {
114340
114351
  position: absolute;
114341
- top: calc(50% - var(--movi-controls-height) / 2);
114352
+ top: 50%;
114342
114353
  left: 50%;
114343
114354
  transform: translate(-50%, -50%);
114344
114355
  display: flex;
@@ -114347,15 +114358,7 @@ class MoviElement extends HTMLElement {
114347
114358
  z-index: 1000;
114348
114359
  pointer-events: none;
114349
114360
  background: transparent;
114350
- }
114351
-
114352
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-loading-indicator,
114353
- :host(:not([controls])) .movi-loading-indicator {
114354
- top: 50%;
114355
- }
114356
-
114357
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-loading-indicator {
114358
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114361
+ transition: top var(--movi-transition-normal);
114359
114362
  }
114360
114363
 
114361
114364
  .movi-loader-container {
@@ -114397,7 +114400,7 @@ class MoviElement extends HTMLElement {
114397
114400
  down adjust this for the other two layout states. */
114398
114401
  .movi-center-play-pause {
114399
114402
  position: absolute;
114400
- top: calc(50% - var(--movi-controls-height) / 2);
114403
+ top: 50%;
114401
114404
  left: 50%;
114402
114405
  transform: translate(-50%, -50%) scale(0.8);
114403
114406
  z-index: 5;
@@ -114426,23 +114429,15 @@ class MoviElement extends HTMLElement {
114426
114429
  transition-delay: 0s;
114427
114430
  }
114428
114431
 
114429
- /* Bar hidden or no controls attribute no chrome to balance
114430
- against, sit at true geometric centre. */
114431
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-center-play-pause,
114432
- :host(:not([controls])) .movi-center-play-pause {
114433
- top: 50%;
114434
- }
114435
-
114436
- /* Bar visible AND title bar visible centre the icon inside
114437
- the visible video band (between title bottom and controls
114438
- top), not the full player. The 44% default biases too far up
114439
- when there's also a title bar to balance the controls; here
114440
- the offset is just half the *difference* between the two
114441
- chrome heights (title ≈ 52px, controls 72px → ~10px above
114442
- centre on desktop). calc() keeps it right on mobile where
114443
- --movi-controls-height drops to 64px too. */
114444
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-center-play-pause {
114445
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114432
+ /* Default (the base rule) is the true geometric centre. When the controls
114433
+ bar is visible, lift the centre button AND the loading spinner a little
114434
+ so they read as centred in the band above the bar. Keyed off a host
114435
+ class toggled in show/hideControls, because :host:has() checks against
114436
+ the shadow tree don't apply in every engine (notably Electron). The
114437
+ transition on top (in each base rule) animates the move. */
114438
+ :host(.movi-bar-visible) .movi-center-play-pause,
114439
+ :host(.movi-bar-visible) .movi-loading-indicator {
114440
+ top: calc(50% - var(--movi-controls-height) / 4);
114446
114441
  }
114447
114442
 
114448
114443
  .movi-center-play-pause:hover {
@@ -116041,6 +116036,7 @@ class MoviElement extends HTMLElement {
116041
116036
  });
116042
116037
  };
116043
116038
  connectedCallback() {
116039
+ this.hasAttribute("tabindex") || (this.tabIndex = 0);
116044
116040
  const A2 = this.getAttribute("src");
116045
116041
  this._src = A2 || null, this._autoplay = this.hasAttribute("autoplay"), this._controls = this.hasAttribute("controls"), this._loop = this.hasAttribute("loop"), this._muted = this.hasAttribute("muted"), this._playsinline = this.hasAttribute("playsinline"), this._preload = this.getAttribute("preload") || "auto", this._poster = this.getAttribute("poster") || "";
116046
116042
  const t2 = this.getAttribute("volume");
@@ -116487,7 +116483,11 @@ class MoviElement extends HTMLElement {
116487
116483
  };
116488
116484
  this.player.trackManager.on("tracksChange", n2), this.eventHandlers.set("tracksChange", () => this.player?.trackManager.off("tracksChange", n2));
116489
116485
  const b2 = (A3) => {
116490
- if (v.info(jj, `stateChange: ${A3}`), "playing" === A3 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A3 })), this.updateLoadingIndicator(A3), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A3 && "ended" !== A3 && "error" !== A3 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A3) {
116486
+ if (v.info(jj, `stateChange: ${A3}`), null !== this._pendingSeek && ("ready" === A3 || "paused" === A3 || "playing" === A3)) {
116487
+ const A4 = this._pendingSeek;
116488
+ this._pendingSeek = null, this.currentTime = A4;
116489
+ }
116490
+ if ("playing" === A3 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A3 })), this.updateLoadingIndicator(A3), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A3 && "ended" !== A3 && "error" !== A3 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A3) {
116491
116491
  const A4 = this._autoplayStarting;
116492
116492
  if (this._autoplayStarting = false, this._resumeDialogPending && !this._hasEverPlayed && (this._resumeDialogPending = false, this.maybeShowResumeDialog()), this._hasEverPlayed = true, this._loopRestartInFlight = false, this.dispatchEvent(new Event("play")), A4) return this.hideControls(), this.updatePlayPauseIcon(), this._ambientMode && (this._ambientSampleInterval = 200), void ("mediaSession" in navigator && this._title && (navigator.mediaSession.metadata = new MediaMetadata({ title: this._title })));
116493
116493
  window.setTimeout(() => {
@@ -117265,10 +117265,11 @@ class MoviElement extends HTMLElement {
117265
117265
  return this.player?.getCurrentTime() || 0;
117266
117266
  }
117267
117267
  set currentTime(A2) {
117268
- if (!this.player) return;
117268
+ if (!this.player) return void (this._pendingSeek = A2);
117269
117269
  A2 = this.clampToBufferedWindow(A2);
117270
117270
  const t2 = this.player.getState();
117271
- v.info(jj, `currentTime setter: value=${A2.toFixed(2)}, state=${t2}, isSeeking=${this.isSeeking}`), "ready" === t2 || "playing" === t2 || "paused" === t2 || "ended" === t2 || "seeking" === t2 || "buffering" === t2 ? this.isSeeking ? this.pendingSeekTarget = A2 : (this.isSeeking = true, this.player.seek(A2).then(() => {
117271
+ if (v.info(jj, `currentTime setter: value=${A2.toFixed(2)}, state=${t2}, isSeeking=${this.isSeeking}`), "ready" !== t2 && "playing" !== t2 && "paused" !== t2 && "ended" !== t2 && "seeking" !== t2 && "buffering" !== t2) return v.info(jj, `Seek held until ready state=${t2}`), void (this._pendingSeek = A2);
117272
+ this._pendingSeek = null, this.isSeeking ? this.pendingSeekTarget = A2 : (this.isSeeking = true, this.player.seek(A2).then(() => {
117272
117273
  v.info(jj, `Seek resolved: value=${A2.toFixed(2)}, newState=${this.player?.getState()}`);
117273
117274
  }).catch((t3) => {
117274
117275
  v.error(jj, `Seek error: value=${A2.toFixed(2)}`, t3);
@@ -117277,7 +117278,7 @@ class MoviElement extends HTMLElement {
117277
117278
  const A3 = this.pendingSeekTarget;
117278
117279
  this.pendingSeekTarget = null, this.currentTime = A3;
117279
117280
  }
117280
- })) : v.warn(jj, `Seek blocked — state=${t2} not allowed`);
117281
+ }));
117281
117282
  }
117282
117283
  get renderer() {
117283
117284
  return this._renderer;
package/dist/index.cjs CHANGED
@@ -49950,7 +49950,7 @@ class MoviVideoDecoder {
49950
49950
  }
49951
49951
  async configure(A, v, b = 0) {
49952
49952
  if (this.currentTrack = A, this.targetFps = b, this.currentProfile = A.profile, this.useSoftware = false, this.requiresSoftware = false, this.isAnnexBSource = false, this.openGopErrorCount = 0, this.hardwareRetryCount = 0, this.lastHardwareRetryTime = 0, this.swDecoder && (this.swDecoder.close(), this.swDecoder = null), this.forceSoftware) return Logger$1.info(TAG$h, "Force software decoding enabled, using WASM decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49953
- if (!("VideoDecoder" in window)) return Logger$1.error(TAG$h, "WebCodecs VideoDecoder not supported"), false;
49953
+ if (!("VideoDecoder" in window)) return Logger$1.warn(TAG$h, "WebCodecs VideoDecoder not supported — falling back to software decoder"), this.useSoftware = true, this.initSoftwareDecoder();
49954
49954
  let j = CodecParser.getCodecString(A.codec, A.extradata);
49955
49955
  if (j || (Logger$1.debug(TAG$h, "CodecParser returned null, falling back to manual mapping"), j = this.mapCodecToWebCodecs(A.codec, A.width, A.height, A.profile, A.level)), !j) return Logger$1.error(TAG$h, `Unsupported codec: ${A.codec}`), false;
49956
49956
  const s2 = { codec: j, codedWidth: A.width, codedHeight: A.height, hardwareAcceleration: "prefer-hardware" };
@@ -108886,6 +108886,7 @@ class MoviElement extends HTMLElement {
108886
108886
  isOverControls = false;
108887
108887
  isSeeking = false;
108888
108888
  pendingSeekTarget = null;
108889
+ _pendingSeek = null;
108889
108890
  isDragging = false;
108890
108891
  isTouchDragging = false;
108891
108892
  touchStartX = 0;
@@ -109011,7 +109012,7 @@ class MoviElement extends HTMLElement {
109011
109012
  return ["src", "autoplay", "controls", "loop", "muted", "playsinline", "preload", "poster", "width", "height", "crossorigin", "volume", "playbackrate", "subtitledelay", "subtitlesize", "subtitlecolor", "subtitlebg", "subtitleedge", "ambientmode", "ambientwrapper", "renderer", "objectfit", "thumb", "hdr", "theme", "sw", "fps", "gesturefs", "nohotkeys", "startat", "fastseek", "doubletap", "themecolor", "buffersize", "title", "showtitle", "resume", "stablevolume", "encrypted", "tokenurl", "videourl", "videoid", "drm", "licenseurl", "licenseheaders", "headers", "audioonly", "lcevc", "lcevcurl", "postertime"];
109012
109013
  }
109013
109014
  constructor() {
109014
- super(), this.tabIndex = 0, Logger$1.setLevel(LogLevel.DEBUG);
109015
+ super(), Logger$1.setLevel(LogLevel.DEBUG);
109015
109016
  const A = this.attachShadow({ mode: "open" });
109016
109017
  this.canvas = document.createElement("canvas"), this.canvas.style.width = "100%", this.canvas.style.height = "100%", this.canvas.style.display = "block", this.canvas.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), A.appendChild(this.canvas), this.video = document.createElement("video"), this.video.style.width = "100%", this.video.style.height = "100%", this.video.style.display = "none", this.video.style.objectFit = "contain", this.video.oncontextmenu = (A2) => (A2.preventDefault(), A2.stopPropagation(), false), A.appendChild(this.video), this.posterElement = document.createElement("img"), this.posterElement.className = "movi-poster-overlay", this.posterElement.crossOrigin = "anonymous", this.posterElement.referrerPolicy = "no-referrer", this.posterElement.decoding = "async", this.posterElement.addEventListener("error", () => {
109017
109018
  const A2 = this.posterElement.src.match(/\/vi\/([\w-]+)\/maxresdefault\.jpg/);
@@ -110878,6 +110879,7 @@ class MoviElement extends HTMLElement {
110878
110879
  const A2 = this.player?.getState();
110879
110880
  "playing" !== A2 && "buffering" !== A2 && b.classList.add("movi-center-visible");
110880
110881
  }
110882
+ this._hasEverPlayed ? this.classList.add("movi-bar-visible") : this.classList.remove("movi-bar-visible");
110881
110883
  const E = this.shadowRoot?.querySelector(".movi-controls-bar"), F = E?.offsetHeight ?? 80, k = this.shadowRoot?.querySelector(".movi-timeline-panel");
110882
110884
  let x = F + 20;
110883
110885
  if (k && "none" !== k.style.display ? (k.style.bottom = `${F + 20}px`, requestAnimationFrame(() => {
@@ -110967,7 +110969,7 @@ class MoviElement extends HTMLElement {
110967
110969
  b2 && (b2.style.cursor = "none");
110968
110970
  }
110969
110971
  const v = this.shadowRoot?.querySelector(".movi-center-play-pause"), b = this.player?.getState();
110970
- v && "playing" === b && v.classList.remove("movi-center-visible");
110972
+ v && "playing" === b && v.classList.remove("movi-center-visible"), this.classList.remove("movi-bar-visible");
110971
110973
  const j = this.shadowRoot?.querySelector(".movi-timeline-panel");
110972
110974
  if (j && (j.style.bottom = "12px"), this.player && (j && "none" !== j.style.display ? requestAnimationFrame(() => {
110973
110975
  const A2 = j.offsetHeight || 0;
@@ -111149,6 +111151,14 @@ class MoviElement extends HTMLElement {
111149
111151
  -moz-osx-font-smoothing: grayscale;
111150
111152
  }
111151
111153
 
111154
+ /* The :host{display:block} above is an author rule, so it beats the
111155
+ UA stylesheet's [hidden]{display:none}. Without this, the standard
111156
+ hidden attribute does nothing and the player stays visible. Honour
111157
+ it explicitly. !important so source-order/specificity can't regress it. */
111158
+ :host([hidden]) {
111159
+ display: none !important;
111160
+ }
111161
+
111152
111162
  /* Light Theme Override */
111153
111163
  :host([theme="light"]) {
111154
111164
  --movi-primary: #7c3aed; /* Vibrancy boost for light theme */
@@ -113737,12 +113747,13 @@ class MoviElement extends HTMLElement {
113737
113747
  height: 64px !important;
113738
113748
  }
113739
113749
 
113740
- /* Disable animations on mobile */
113750
+ /* Keyframe animations off on small/compact layouts for perf, but keep
113751
+ CSS transitions so the controls + centre button still move smoothly
113752
+ here too (the PiP / mini player is a small container). */
113741
113753
  .movi-controls-overlay,
113742
113754
  .movi-center-play-pause,
113743
113755
  .movi-btn,
113744
113756
  .movi-progress-handle {
113745
- transition: none !important;
113746
113757
  animation: none !important;
113747
113758
  transform: none !important;
113748
113759
  }
@@ -114234,7 +114245,7 @@ class MoviElement extends HTMLElement {
114234
114245
  button's bar-hidden and both-bars-visible rules exactly. */
114235
114246
  .movi-loading-indicator {
114236
114247
  position: absolute;
114237
- top: calc(50% - var(--movi-controls-height) / 2);
114248
+ top: 50%;
114238
114249
  left: 50%;
114239
114250
  transform: translate(-50%, -50%);
114240
114251
  display: flex;
@@ -114243,15 +114254,7 @@ class MoviElement extends HTMLElement {
114243
114254
  z-index: 1000;
114244
114255
  pointer-events: none;
114245
114256
  background: transparent;
114246
- }
114247
-
114248
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-loading-indicator,
114249
- :host(:not([controls])) .movi-loading-indicator {
114250
- top: 50%;
114251
- }
114252
-
114253
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-loading-indicator {
114254
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114257
+ transition: top var(--movi-transition-normal);
114255
114258
  }
114256
114259
 
114257
114260
  .movi-loader-container {
@@ -114293,7 +114296,7 @@ class MoviElement extends HTMLElement {
114293
114296
  down adjust this for the other two layout states. */
114294
114297
  .movi-center-play-pause {
114295
114298
  position: absolute;
114296
- top: calc(50% - var(--movi-controls-height) / 2);
114299
+ top: 50%;
114297
114300
  left: 50%;
114298
114301
  transform: translate(-50%, -50%) scale(0.8);
114299
114302
  z-index: 5;
@@ -114322,23 +114325,15 @@ class MoviElement extends HTMLElement {
114322
114325
  transition-delay: 0s;
114323
114326
  }
114324
114327
 
114325
- /* Bar hidden or no controls attribute no chrome to balance
114326
- against, sit at true geometric centre. */
114327
- :host:has(.movi-controls-container.movi-controls-hidden) .movi-center-play-pause,
114328
- :host(:not([controls])) .movi-center-play-pause {
114329
- top: 50%;
114330
- }
114331
-
114332
- /* Bar visible AND title bar visible centre the icon inside
114333
- the visible video band (between title bottom and controls
114334
- top), not the full player. The 44% default biases too far up
114335
- when there's also a title bar to balance the controls; here
114336
- the offset is just half the *difference* between the two
114337
- chrome heights (title ≈ 52px, controls 72px → ~10px above
114338
- centre on desktop). calc() keeps it right on mobile where
114339
- --movi-controls-height drops to 64px too. */
114340
- :host:has(.movi-controls-container.movi-controls-visible):has(.movi-title-bar.movi-title-visible) .movi-center-play-pause {
114341
- top: calc(50% - (var(--movi-controls-height) - 52px) / 2);
114328
+ /* Default (the base rule) is the true geometric centre. When the controls
114329
+ bar is visible, lift the centre button AND the loading spinner a little
114330
+ so they read as centred in the band above the bar. Keyed off a host
114331
+ class toggled in show/hideControls, because :host:has() checks against
114332
+ the shadow tree don't apply in every engine (notably Electron). The
114333
+ transition on top (in each base rule) animates the move. */
114334
+ :host(.movi-bar-visible) .movi-center-play-pause,
114335
+ :host(.movi-bar-visible) .movi-loading-indicator {
114336
+ top: calc(50% - var(--movi-controls-height) / 4);
114342
114337
  }
114343
114338
 
114344
114339
  .movi-center-play-pause:hover {
@@ -115937,6 +115932,7 @@ class MoviElement extends HTMLElement {
115937
115932
  });
115938
115933
  };
115939
115934
  connectedCallback() {
115935
+ this.hasAttribute("tabindex") || (this.tabIndex = 0);
115940
115936
  const A = this.getAttribute("src");
115941
115937
  this._src = A || null, this._autoplay = this.hasAttribute("autoplay"), this._controls = this.hasAttribute("controls"), this._loop = this.hasAttribute("loop"), this._muted = this.hasAttribute("muted"), this._playsinline = this.hasAttribute("playsinline"), this._preload = this.getAttribute("preload") || "auto", this._poster = this.getAttribute("poster") || "";
115942
115938
  const v = this.getAttribute("volume");
@@ -116383,7 +116379,11 @@ class MoviElement extends HTMLElement {
116383
116379
  };
116384
116380
  this.player.trackManager.on("tracksChange", b), this.eventHandlers.set("tracksChange", () => this.player?.trackManager.off("tracksChange", b));
116385
116381
  const j = (A2) => {
116386
- if (Logger$1.info(TAG, `stateChange: ${A2}`), "playing" === A2 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A2 })), this.updateLoadingIndicator(A2), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A2 && "ended" !== A2 && "error" !== A2 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A2) {
116382
+ if (Logger$1.info(TAG, `stateChange: ${A2}`), null !== this._pendingSeek && ("ready" === A2 || "paused" === A2 || "playing" === A2)) {
116383
+ const A3 = this._pendingSeek;
116384
+ this._pendingSeek = null, this.currentTime = A3;
116385
+ }
116386
+ if ("playing" === A2 && this.posterElement && (this._qualitySwitchInProgress && (this._hideSnapshotPoster(), this._qualitySwitchInProgress = false, this._switchResumeTime = 0, this._switchResumeDuration = 0), this.posterElement.style.display = "none"), this.dispatchEvent(new CustomEvent("statechange", { detail: A2 })), this.updateLoadingIndicator(A2), this.updateControlsState(), this.updatePlayPauseIcon(), !this._autoplayStarting || "paused" !== A2 && "ended" !== A2 && "error" !== A2 || (this._autoplayStarting = false, this.updatePlayPauseIcon()), "playing" === A2) {
116387
116387
  const A3 = this._autoplayStarting;
116388
116388
  if (this._autoplayStarting = false, this._resumeDialogPending && !this._hasEverPlayed && (this._resumeDialogPending = false, this.maybeShowResumeDialog()), this._hasEverPlayed = true, this._loopRestartInFlight = false, this.dispatchEvent(new Event("play")), A3) return this.hideControls(), this.updatePlayPauseIcon(), this._ambientMode && (this._ambientSampleInterval = 200), void ("mediaSession" in navigator && this._title && (navigator.mediaSession.metadata = new MediaMetadata({ title: this._title })));
116389
116389
  window.setTimeout(() => {
@@ -117161,10 +117161,11 @@ class MoviElement extends HTMLElement {
117161
117161
  return this.player?.getCurrentTime() || 0;
117162
117162
  }
117163
117163
  set currentTime(A) {
117164
- if (!this.player) return;
117164
+ if (!this.player) return void (this._pendingSeek = A);
117165
117165
  A = this.clampToBufferedWindow(A);
117166
117166
  const v = this.player.getState();
117167
- Logger$1.info(TAG, `currentTime setter: value=${A.toFixed(2)}, state=${v}, isSeeking=${this.isSeeking}`), "ready" === v || "playing" === v || "paused" === v || "ended" === v || "seeking" === v || "buffering" === v ? this.isSeeking ? this.pendingSeekTarget = A : (this.isSeeking = true, this.player.seek(A).then(() => {
117167
+ if (Logger$1.info(TAG, `currentTime setter: value=${A.toFixed(2)}, state=${v}, isSeeking=${this.isSeeking}`), "ready" !== v && "playing" !== v && "paused" !== v && "ended" !== v && "seeking" !== v && "buffering" !== v) return Logger$1.info(TAG, `Seek held until ready state=${v}`), void (this._pendingSeek = A);
117168
+ this._pendingSeek = null, this.isSeeking ? this.pendingSeekTarget = A : (this.isSeeking = true, this.player.seek(A).then(() => {
117168
117169
  Logger$1.info(TAG, `Seek resolved: value=${A.toFixed(2)}, newState=${this.player?.getState()}`);
117169
117170
  }).catch((v2) => {
117170
117171
  Logger$1.error(TAG, `Seek error: value=${A.toFixed(2)}`, v2);
@@ -117173,7 +117174,7 @@ class MoviElement extends HTMLElement {
117173
117174
  const A2 = this.pendingSeekTarget;
117174
117175
  this.pendingSeekTarget = null, this.currentTime = A2;
117175
117176
  }
117176
- })) : Logger$1.warn(TAG, `Seek blocked — state=${v} not allowed`);
117177
+ }));
117177
117178
  }
117178
117179
  get renderer() {
117179
117180
  return this._renderer;