@shortkitsdk/web 0.3.0 → 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.
@@ -603,16 +603,35 @@
603
603
  player._skCurrentUrl = streamingUrl;
604
604
  }
605
605
  /**
606
- * Promote a preload player to active: raise buffer limits and uncap ABR.
607
- * Safe to call even if the HLS instance was already created with active config.
606
+ * Promote a preload player to active: raise buffer limits, uncap ABR, and
607
+ * force an immediate quality upgrade.
608
+ *
609
+ * Preload streams start at level 0 (lowest rendition) to conserve bandwidth.
610
+ * When attachStream() is called again with isActive=true for the same URL it
611
+ * early-returns, so the HLS instance keeps its startLevel:0 config. The
612
+ * already-buffered low-quality segments play through before any higher-quality
613
+ * segments arrive — causing a visible low-res start even on fast connections.
614
+ *
615
+ * Fix: lock HLS to the highest level, then seek-in-place to flush the
616
+ * low-quality buffer so the player immediately re-fetches at high quality.
617
+ * Re-enable ABR after a few seconds so slow connections self-correct.
608
618
  */
609
619
  promoteToActive(itemId) {
610
620
  const hls = this.hlsInstances.get(itemId);
611
- if (hls) {
612
- hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
613
- hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
614
- hls.autoLevelCapping = -1;
615
- hls.nextAutoLevel = -1;
621
+ if (!hls) return;
622
+ hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
623
+ hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
624
+ hls.autoLevelCapping = -1;
625
+ const top = hls.levels ? hls.levels.length - 1 : -1;
626
+ if (top > 0 && hls.currentLevel < top) {
627
+ hls.currentLevel = top;
628
+ const player = this.assignments.get(itemId);
629
+ if (player) {
630
+ player.currentTime = player.currentTime;
631
+ }
632
+ setTimeout(() => {
633
+ if (!hls.destroyed) hls.currentLevel = -1;
634
+ }, 4e3);
616
635
  }
617
636
  }
618
637
  /** Pause, destroy HLS, and remove player from DOM and pool tracking. */
@@ -908,7 +927,7 @@
908
927
  }
909
928
  setMuted(muted) {
910
929
  this.isMuted = muted;
911
- for (const [, player] of this.pool._players) {
930
+ for (const [, player] of this.pool.assignments) {
912
931
  player.muted = muted;
913
932
  }
914
933
  this._pushPlayerState({ isMuted: muted });
@@ -921,7 +940,7 @@
921
940
  }
922
941
  setPlaybackRate(rate) {
923
942
  this.playbackRate = rate;
924
- for (const [, player] of this.pool._players) {
943
+ for (const [, player] of this.pool.assignments) {
925
944
  player.playbackRate = rate;
926
945
  }
927
946
  this._pushPlayerState({ playbackRate: rate });
@@ -1398,11 +1417,10 @@
1398
1417
  const player = this.pool.getPlayer(id);
1399
1418
  if (player) {
1400
1419
  player.pause();
1401
- player.style.opacity = "0";
1402
1420
  player._skRevealedFor = null;
1403
1421
  }
1404
1422
  this._stopTimeLoop(id);
1405
- this._clearOverlay(id);
1423
+ this._detachOverlay(id);
1406
1424
  this._sk._tracker.deactivateContent();
1407
1425
  }
1408
1426
  // --- Time loop ---
@@ -1584,14 +1602,20 @@
1584
1602
  }
1585
1603
  }
1586
1604
  /** Clear the overlay container's DOM and unsubscribe tracked listeners. */
1587
- _clearOverlay(itemId) {
1605
+ /** Unsubscribe overlay event listeners without clearing DOM. */
1606
+ _detachOverlay(itemId) {
1588
1607
  const entry = this._overlayContainers.get(itemId);
1589
1608
  if (!entry) return;
1590
1609
  if (entry.unsub) {
1591
1610
  entry.unsub();
1592
1611
  entry.unsub = null;
1593
1612
  }
1594
- entry.el.innerHTML = "";
1613
+ }
1614
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
1615
+ _clearOverlay(itemId) {
1616
+ this._detachOverlay(itemId);
1617
+ const entry = this._overlayContainers.get(itemId);
1618
+ if (entry) entry.el.innerHTML = "";
1595
1619
  }
1596
1620
  }
1597
1621
  class EmbeddedFeedManager {
@@ -1640,6 +1664,7 @@
1640
1664
  this._setupScrollEnd();
1641
1665
  this._setupVisibilityHandler();
1642
1666
  this._setupCellHeightObserver();
1667
+ this._setupDebugPanel();
1643
1668
  if (startIndex > 0 && startIndex < items.length) {
1644
1669
  const targetEl = this.itemEls.get(items[startIndex].id);
1645
1670
  if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
@@ -1662,6 +1687,11 @@
1662
1687
  this._resizeObserver = null;
1663
1688
  }
1664
1689
  if (this._visHandler) document.removeEventListener("visibilitychange", this._visHandler);
1690
+ if (this._debugKeyHandler) document.removeEventListener("keydown", this._debugKeyHandler);
1691
+ if (this._debugPanel) {
1692
+ this._debugPanel.remove();
1693
+ this._debugPanel = null;
1694
+ }
1665
1695
  this.container.innerHTML = "";
1666
1696
  this.itemEls.clear();
1667
1697
  this.items = [];
@@ -1705,7 +1735,7 @@
1705
1735
  }
1706
1736
  setMuted(muted) {
1707
1737
  this.isMuted = muted;
1708
- for (const [, player] of this.pool._players) {
1738
+ for (const [, player] of this.pool.assignments) {
1709
1739
  player.muted = muted;
1710
1740
  }
1711
1741
  this._pushPlayerState({ isMuted: muted });
@@ -1718,7 +1748,7 @@
1718
1748
  }
1719
1749
  setPlaybackRate(rate) {
1720
1750
  this.playbackRate = rate;
1721
- for (const [, player] of this.pool._players) {
1751
+ for (const [, player] of this.pool.assignments) {
1722
1752
  player.playbackRate = rate;
1723
1753
  }
1724
1754
  this._pushPlayerState({ playbackRate: rate });
@@ -1990,11 +2020,52 @@
1990
2020
  const p = this.pool.getPlayer(id);
1991
2021
  if (p) {
1992
2022
  p.pause();
1993
- p.style.opacity = "0";
1994
2023
  p._skRevealedFor = null;
1995
2024
  }
1996
2025
  this._stopTimeLoop(id);
1997
- this._clearOverlay(id);
2026
+ this._detachOverlay(id);
2027
+ }
2028
+ // --- Debug panel (toggle with 'D' key) ---
2029
+ _setupDebugPanel() {
2030
+ const panel = document.createElement("div");
2031
+ panel.id = "sk-debug-panel";
2032
+ panel.style.cssText = "position:fixed;top:12px;right:12px;z-index:99999;background:rgba(0,0,0,.85);color:#0f0;font:12px/1.5 monospace;padding:10px 14px;border-radius:8px;pointer-events:none;display:none;min-width:220px;";
2033
+ document.body.appendChild(panel);
2034
+ this._debugPanel = panel;
2035
+ this._debugVisible = false;
2036
+ this._debugKeyHandler = (e) => {
2037
+ if (e.key === "d" || e.key === "D") {
2038
+ this._debugVisible = !this._debugVisible;
2039
+ panel.style.display = this._debugVisible ? "block" : "none";
2040
+ }
2041
+ };
2042
+ document.addEventListener("keydown", this._debugKeyHandler);
2043
+ }
2044
+ _updateDebugPanel() {
2045
+ if (!this._debugVisible || !this._debugPanel) return;
2046
+ const hls = this.pool.hlsInstances.get(this.activeItemId);
2047
+ if (!hls || !hls.levels || !hls.levels.length) {
2048
+ this._debugPanel.innerHTML = "HLS: no levels loaded";
2049
+ return;
2050
+ }
2051
+ const level = hls.levels[hls.currentLevel] || {};
2052
+ const bw = hls.bandwidthEstimate ? (hls.bandwidthEstimate / 1e6).toFixed(2) : "?";
2053
+ const lines = [
2054
+ `<b style="color:#fff">HLS Debug</b>`,
2055
+ `Level: ${hls.currentLevel} / ${hls.levels.length - 1}`,
2056
+ `Resolution: ${level.width || "?"}x${level.height || "?"}`,
2057
+ `Bitrate: ${level.bitrate ? (level.bitrate / 1e3).toFixed(0) + " kbps" : "?"}`,
2058
+ `BW estimate: ${bw} Mbps`,
2059
+ `Auto cap: ${hls.autoLevelCapping}`,
2060
+ `Next level: ${hls.nextLevel}`,
2061
+ `<span style="color:#888">Levels:</span>`
2062
+ ];
2063
+ for (let i = 0; i < hls.levels.length; i++) {
2064
+ const l = hls.levels[i];
2065
+ const marker = i === hls.currentLevel ? ' <b style="color:#0ff">◀</b>' : "";
2066
+ lines.push(` ${i}: ${l.width}x${l.height} @ ${(l.bitrate / 1e3).toFixed(0)}k${marker}`);
2067
+ }
2068
+ this._debugPanel.innerHTML = lines.join("<br>");
1998
2069
  }
1999
2070
  // --- Time loop (state emission only, no DOM updates) ---
2000
2071
  _startTimeLoop(id, el, player) {
@@ -2002,6 +2073,7 @@
2002
2073
  const tick = () => {
2003
2074
  if (this._destroyed) return;
2004
2075
  if (!player || player.paused) {
2076
+ this._updateDebugPanel();
2005
2077
  this.rafIds.set(id, requestAnimationFrame(tick));
2006
2078
  return;
2007
2079
  }
@@ -2027,6 +2099,7 @@
2027
2099
  player.play().catch(() => {
2028
2100
  });
2029
2101
  }
2102
+ this._updateDebugPanel();
2030
2103
  this.rafIds.set(id, requestAnimationFrame(tick));
2031
2104
  };
2032
2105
  this.rafIds.set(id, requestAnimationFrame(tick));
@@ -2193,14 +2266,20 @@
2193
2266
  } catch (e) {
2194
2267
  }
2195
2268
  }
2196
- _clearOverlay(itemId) {
2269
+ /** Unsubscribe overlay event listeners without clearing DOM. */
2270
+ _detachOverlay(itemId) {
2197
2271
  const entry = this._overlayContainers.get(itemId);
2198
2272
  if (!entry) return;
2199
2273
  if (entry.unsub) {
2200
2274
  entry.unsub();
2201
2275
  entry.unsub = null;
2202
2276
  }
2203
- entry.el.innerHTML = "";
2277
+ }
2278
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
2279
+ _clearOverlay(itemId) {
2280
+ this._detachOverlay(itemId);
2281
+ const entry = this._overlayContainers.get(itemId);
2282
+ if (entry) entry.el.innerHTML = "";
2204
2283
  }
2205
2284
  }
2206
2285
  const MuteOnSvg = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>';
@@ -2550,6 +2629,7 @@
2550
2629
  this._isOpen = true;
2551
2630
  this._onClose = onClose;
2552
2631
  this._openItemId = item.id;
2632
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2553
2633
  const sourceRect = sourceEl.getBoundingClientRect();
2554
2634
  const sourceRadius = parseFloat(getComputedStyle(sourceEl).borderRadius) || 12;
2555
2635
  this.overlayEl.classList.add("skp-active");
@@ -2594,6 +2674,21 @@
2594
2674
  feedEl.style.transform = "none";
2595
2675
  feedEl.style.borderRadius = `${targetRadius}px`;
2596
2676
  });
2677
+ this.feedManager.startObserver();
2678
+ this.feedManager._activateItem(item.id);
2679
+ this.overlayEl.classList.add("skp-feed-ready");
2680
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2681
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skp-feed-close, .sk-sidebar")];
2682
+ if (itemOverlay) chromeEls.push(itemOverlay);
2683
+ chromeEls.forEach((el) => {
2684
+ el.style.opacity = "0";
2685
+ el.style.transition = "none";
2686
+ });
2687
+ this.overlayEl.getBoundingClientRect();
2688
+ chromeEls.forEach((el) => {
2689
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2690
+ el.style.opacity = "1";
2691
+ });
2597
2692
  await this._waitForTransitionEnd(feedEl, 450);
2598
2693
  feedEl.classList.remove("skp-flip-animating");
2599
2694
  feedEl.style.transformOrigin = "";
@@ -2601,9 +2696,10 @@
2601
2696
  feedEl.style.scrollSnapType = "";
2602
2697
  feedEl.style.transform = "";
2603
2698
  feedEl.style.borderRadius = "";
2604
- this.feedManager.startObserver();
2605
- this.feedManager._activateItem(item.id);
2606
- this.overlayEl.classList.add("skp-feed-ready");
2699
+ chromeEls.forEach((el) => {
2700
+ el.style.opacity = "";
2701
+ el.style.transition = "";
2702
+ });
2607
2703
  this._escHandler = (e) => {
2608
2704
  if (e.key === "Escape") this.close();
2609
2705
  };
@@ -2616,33 +2712,44 @@
2616
2712
  const feedWrapper = feedEl.parentNode;
2617
2713
  const feedRect = feedEl.getBoundingClientRect();
2618
2714
  const activeItemId = this.feedManager?.activeItemId;
2619
- let transferBack = false, transferVideo = null, transferHls = null;
2620
- if (activeItemId === this._openItemId) {
2621
- const ejected = this.feedManager?.pool.ejectPlayer(activeItemId);
2622
- if (ejected) {
2623
- transferBack = true;
2624
- transferVideo = ejected.video;
2625
- transferHls = ejected.hls;
2626
- }
2627
- }
2715
+ const canTransfer = activeItemId === this._openItemId;
2628
2716
  let targetRect = null;
2629
2717
  if (this._onClose) targetRect = this._onClose("getSourceRect");
2630
2718
  if (feedRect && targetRect) {
2631
- if (this.feedManager?.activeItemId && !transferBack) {
2719
+ if (this.feedManager?.activeItemId && !canTransfer) {
2632
2720
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2633
2721
  }
2722
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2723
+ if (targetItemId && this.feedManager) {
2724
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2725
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2726
+ }
2727
+ const savedScrollTop = feedEl.scrollTop;
2634
2728
  feedEl.style.position = "fixed";
2635
2729
  feedEl.style.left = `${feedRect.left}px`;
2636
2730
  feedEl.style.top = `${feedRect.top}px`;
2637
2731
  feedEl.style.width = `${feedRect.width}px`;
2638
2732
  feedEl.style.height = `${feedRect.height}px`;
2639
- feedEl.style.zIndex = "9999";
2733
+ feedEl.style.zIndex = String(getComputedStyle(this.overlayEl).zIndex || 9999);
2640
2734
  feedEl.style.margin = "0";
2641
2735
  feedEl.style.flex = "none";
2642
2736
  feedEl.style.aspectRatio = "unset";
2643
2737
  feedEl.style.maxHeight = "none";
2644
2738
  document.body.appendChild(feedEl);
2739
+ feedEl.scrollTop = savedScrollTop;
2645
2740
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2741
+ let thumbOverlay = null;
2742
+ if (this._openThumbnailUrl) {
2743
+ const crossfadeItemId = canTransfer ? this._openItemId : activeItemId;
2744
+ const crossfadeEl = crossfadeItemId && this.feedManager?.itemEls.get(crossfadeItemId);
2745
+ const videoContainer = crossfadeEl?.querySelector('[data-ref="videoContainer"]');
2746
+ if (videoContainer) {
2747
+ thumbOverlay = document.createElement("img");
2748
+ thumbOverlay.src = this._openThumbnailUrl;
2749
+ thumbOverlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s cubic-bezier(.32,.72,0,1);pointer-events:none;z-index:2;";
2750
+ videoContainer.appendChild(thumbOverlay);
2751
+ }
2752
+ }
2646
2753
  const scaleX = targetRect.width / feedRect.width;
2647
2754
  const scaleY = targetRect.height / feedRect.height;
2648
2755
  const translateX = targetRect.left - feedRect.left;
@@ -2652,29 +2759,58 @@
2652
2759
  feedEl.style.transformOrigin = "0 0";
2653
2760
  feedEl.style.overflow = "hidden";
2654
2761
  feedEl.style.scrollSnapType = "none";
2762
+ feedEl.style.pointerEvents = "none";
2655
2763
  feedEl.getBoundingClientRect();
2656
2764
  feedEl.classList.add("skp-flip-animating");
2657
2765
  requestAnimationFrame(() => {
2658
2766
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2659
2767
  feedEl.style.borderRadius = `${endRadius}px`;
2768
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2660
2769
  });
2661
2770
  await this._waitForTransitionEnd(feedEl, 450);
2771
+ if (thumbOverlay) thumbOverlay.remove();
2772
+ let transferVideo = null;
2773
+ let transferHls = null;
2774
+ if (canTransfer) {
2775
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2776
+ if (ejected) {
2777
+ transferVideo = ejected.video;
2778
+ transferHls = ejected.hls;
2779
+ }
2780
+ }
2662
2781
  feedEl.style.visibility = "hidden";
2663
2782
  feedEl.classList.remove("skp-flip-animating");
2664
- ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2783
+ ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "pointerEvents", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2665
2784
  const sidebarEl = feedWrapper.querySelector(".sk-sidebar");
2666
2785
  feedWrapper.insertBefore(feedEl, sidebarEl);
2667
2786
  feedEl.style.visibility = "";
2787
+ if (this._onClose) {
2788
+ this._onClose("closed", {
2789
+ transferVideo,
2790
+ transferHls,
2791
+ transferItemId: transferVideo ? this._openItemId : null
2792
+ });
2793
+ this._onClose = null;
2794
+ }
2668
2795
  } else {
2669
2796
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2670
- }
2671
- if (this._onClose) {
2672
- this._onClose("closed", {
2673
- transferVideo: transferBack ? transferVideo : null,
2674
- transferHls: transferBack ? transferHls : null,
2675
- transferItemId: transferBack ? this._openItemId : null
2676
- });
2677
- this._onClose = null;
2797
+ if (this._onClose) {
2798
+ let transferVideo = null;
2799
+ let transferHls = null;
2800
+ if (canTransfer) {
2801
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2802
+ if (ejected) {
2803
+ transferVideo = ejected.video;
2804
+ transferHls = ejected.hls;
2805
+ }
2806
+ }
2807
+ this._onClose("closed", {
2808
+ transferVideo,
2809
+ transferHls,
2810
+ transferItemId: transferVideo ? this._openItemId : null
2811
+ });
2812
+ this._onClose = null;
2813
+ }
2678
2814
  }
2679
2815
  if (this.feedManager) {
2680
2816
  this.feedManager.destroy();
@@ -2726,6 +2862,7 @@
2726
2862
  this._onClose = onClose;
2727
2863
  this._openSlotIndex = startIndex;
2728
2864
  this._openItemId = item.id;
2865
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2729
2866
  const slotRect = slotEl.getBoundingClientRect();
2730
2867
  const slotRadius = parseFloat(getComputedStyle(slotEl).borderRadius) || 12;
2731
2868
  if (_isPreview) {
@@ -2772,6 +2909,21 @@
2772
2909
  feedEl.style.transform = "none";
2773
2910
  feedEl.style.borderRadius = `${targetRadius}px`;
2774
2911
  });
2912
+ this.feedManager.startObserver();
2913
+ this.feedManager._activateItem(item.id);
2914
+ this.overlayEl.classList.add("skw-feed-ready");
2915
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2916
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skw-feed-close, .sk-sidebar")];
2917
+ if (itemOverlay) chromeEls.push(itemOverlay);
2918
+ chromeEls.forEach((el) => {
2919
+ el.style.opacity = "0";
2920
+ el.style.transition = "none";
2921
+ });
2922
+ this.overlayEl.getBoundingClientRect();
2923
+ chromeEls.forEach((el) => {
2924
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2925
+ el.style.opacity = "1";
2926
+ });
2775
2927
  await this._waitForTransitionEnd(feedEl, 450);
2776
2928
  feedEl.classList.remove("skw-flip-animating");
2777
2929
  feedEl.style.transformOrigin = "";
@@ -2779,9 +2931,10 @@
2779
2931
  feedEl.style.scrollSnapType = "";
2780
2932
  feedEl.style.transform = "";
2781
2933
  feedEl.style.borderRadius = "";
2782
- this.feedManager.startObserver();
2783
- this.feedManager._activateItem(item.id);
2784
- this.overlayEl.classList.add("skw-feed-ready");
2934
+ chromeEls.forEach((el) => {
2935
+ el.style.opacity = "";
2936
+ el.style.transition = "";
2937
+ });
2785
2938
  this._escHandler = (e) => {
2786
2939
  if (e.key === "Escape") this.close();
2787
2940
  };
@@ -2808,6 +2961,12 @@
2808
2961
  if (this.feedManager?.activeItemId && !canTransfer) {
2809
2962
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2810
2963
  }
2964
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2965
+ if (targetItemId && this.feedManager) {
2966
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2967
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2968
+ }
2969
+ const savedScrollTop = feedEl.scrollTop;
2811
2970
  feedEl.style.position = "fixed";
2812
2971
  feedEl.style.left = `${feedRect.left}px`;
2813
2972
  feedEl.style.top = `${feedRect.top}px`;
@@ -2819,7 +2978,20 @@
2819
2978
  feedEl.style.aspectRatio = "unset";
2820
2979
  feedEl.style.maxHeight = "none";
2821
2980
  document.body.appendChild(feedEl);
2981
+ feedEl.scrollTop = savedScrollTop;
2822
2982
  this.overlayEl.classList.remove("skw-active", "skw-feed-ready");
2983
+ let thumbOverlay = null;
2984
+ if (this._openThumbnailUrl) {
2985
+ const targetItemId2 = canTransfer ? this._openItemId : activeItemId;
2986
+ const targetEl = targetItemId2 && this.feedManager?.itemEls.get(targetItemId2);
2987
+ const videoContainer = targetEl?.querySelector('[data-ref="videoContainer"]');
2988
+ if (videoContainer) {
2989
+ thumbOverlay = document.createElement("img");
2990
+ thumbOverlay.src = this._openThumbnailUrl;
2991
+ thumbOverlay.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s cubic-bezier(.32,.72,0,1);pointer-events:none;z-index:2;";
2992
+ videoContainer.appendChild(thumbOverlay);
2993
+ }
2994
+ }
2823
2995
  const slotRadius = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--skw-radius").trim()) || 12;
2824
2996
  const scaleX = targetSlotRect.width / feedRect.width;
2825
2997
  const scaleY = targetSlotRect.height / feedRect.height;
@@ -2829,13 +3001,16 @@
2829
3001
  feedEl.style.transformOrigin = "0 0";
2830
3002
  feedEl.style.overflow = "hidden";
2831
3003
  feedEl.style.scrollSnapType = "none";
3004
+ feedEl.style.pointerEvents = "none";
2832
3005
  feedEl.getBoundingClientRect();
2833
3006
  feedEl.classList.add("skw-flip-animating");
2834
3007
  requestAnimationFrame(() => {
2835
3008
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2836
3009
  feedEl.style.borderRadius = `${endRadius}px`;
3010
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2837
3011
  });
2838
3012
  await this._waitForTransitionEnd(feedEl, 450);
3013
+ if (thumbOverlay) thumbOverlay.remove();
2839
3014
  let transferVideo = null;
2840
3015
  let transferHls = null;
2841
3016
  if (canTransfer) {
@@ -2860,6 +3035,7 @@
2860
3035
  feedEl.style.transformOrigin = "";
2861
3036
  feedEl.style.overflow = "";
2862
3037
  feedEl.style.scrollSnapType = "";
3038
+ feedEl.style.pointerEvents = "";
2863
3039
  feedEl.style.transform = "";
2864
3040
  feedEl.style.borderRadius = "";
2865
3041
  feedWrapper.insertBefore(feedEl, feedWrapper.firstChild);
@@ -2989,11 +3165,20 @@
2989
3165
  }
2990
3166
  promoteToActive(itemId) {
2991
3167
  const hls = this.hlsInstances.get(itemId);
2992
- if (hls) {
2993
- hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
2994
- hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
2995
- hls.autoLevelCapping = -1;
2996
- hls.nextAutoLevel = -1;
3168
+ if (!hls) return;
3169
+ hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
3170
+ hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
3171
+ hls.autoLevelCapping = -1;
3172
+ const top = hls.levels ? hls.levels.length - 1 : -1;
3173
+ if (top > 0 && hls.currentLevel < top) {
3174
+ hls.currentLevel = top;
3175
+ const player = this.assignments.get(itemId);
3176
+ if (player) {
3177
+ player.currentTime = player.currentTime;
3178
+ }
3179
+ setTimeout(() => {
3180
+ if (!hls.destroyed) hls.currentLevel = -1;
3181
+ }, 4e3);
2997
3182
  }
2998
3183
  }
2999
3184
  _destroyHls(itemId) {
@@ -3598,7 +3783,7 @@
3598
3783
 
3599
3784
  /* Video container */
3600
3785
  .sk-video-container{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background-size:cover;background-position:center}
3601
- .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .15s ease}
3786
+ .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .35s ease}
3602
3787
 
3603
3788
  /* Tap zone */
3604
3789
  .sk-tap-zone{position:absolute;inset:0;z-index:3;cursor:pointer}
@@ -3614,7 +3799,7 @@
3614
3799
 
3615
3800
  /* Widget slot */
3616
3801
  .skw-slot{position:relative;overflow:hidden;cursor:pointer}
3617
- .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .2s ease}
3802
+ .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s ease}
3618
3803
  .skw-slot-thumb{position:absolute;inset:0;background-size:cover;background-position:center}
3619
3804
  .skw-slot-overlay{position:absolute;inset:0;z-index:4;pointer-events:none}
3620
3805
  .skw-slot-overlay>*{pointer-events:auto}