@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.
@@ -601,16 +601,35 @@ class PlayerPool {
601
601
  player._skCurrentUrl = streamingUrl;
602
602
  }
603
603
  /**
604
- * Promote a preload player to active: raise buffer limits and uncap ABR.
605
- * Safe to call even if the HLS instance was already created with active config.
604
+ * Promote a preload player to active: raise buffer limits, uncap ABR, and
605
+ * force an immediate quality upgrade.
606
+ *
607
+ * Preload streams start at level 0 (lowest rendition) to conserve bandwidth.
608
+ * When attachStream() is called again with isActive=true for the same URL it
609
+ * early-returns, so the HLS instance keeps its startLevel:0 config. The
610
+ * already-buffered low-quality segments play through before any higher-quality
611
+ * segments arrive — causing a visible low-res start even on fast connections.
612
+ *
613
+ * Fix: lock HLS to the highest level, then seek-in-place to flush the
614
+ * low-quality buffer so the player immediately re-fetches at high quality.
615
+ * Re-enable ABR after a few seconds so slow connections self-correct.
606
616
  */
607
617
  promoteToActive(itemId) {
608
618
  const hls = this.hlsInstances.get(itemId);
609
- if (hls) {
610
- hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
611
- hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
612
- hls.autoLevelCapping = -1;
613
- hls.nextAutoLevel = -1;
619
+ if (!hls) return;
620
+ hls.config.maxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
621
+ hls.config.maxMaxBufferLength = PlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
622
+ hls.autoLevelCapping = -1;
623
+ const top = hls.levels ? hls.levels.length - 1 : -1;
624
+ if (top > 0 && hls.currentLevel < top) {
625
+ hls.currentLevel = top;
626
+ const player = this.assignments.get(itemId);
627
+ if (player) {
628
+ player.currentTime = player.currentTime;
629
+ }
630
+ setTimeout(() => {
631
+ if (!hls.destroyed) hls.currentLevel = -1;
632
+ }, 4e3);
614
633
  }
615
634
  }
616
635
  /** Pause, destroy HLS, and remove player from DOM and pool tracking. */
@@ -906,7 +925,7 @@ class FeedManager {
906
925
  }
907
926
  setMuted(muted) {
908
927
  this.isMuted = muted;
909
- for (const [, player] of this.pool._players) {
928
+ for (const [, player] of this.pool.assignments) {
910
929
  player.muted = muted;
911
930
  }
912
931
  this._pushPlayerState({ isMuted: muted });
@@ -919,7 +938,7 @@ class FeedManager {
919
938
  }
920
939
  setPlaybackRate(rate) {
921
940
  this.playbackRate = rate;
922
- for (const [, player] of this.pool._players) {
941
+ for (const [, player] of this.pool.assignments) {
923
942
  player.playbackRate = rate;
924
943
  }
925
944
  this._pushPlayerState({ playbackRate: rate });
@@ -1396,11 +1415,10 @@ class FeedManager {
1396
1415
  const player = this.pool.getPlayer(id);
1397
1416
  if (player) {
1398
1417
  player.pause();
1399
- player.style.opacity = "0";
1400
1418
  player._skRevealedFor = null;
1401
1419
  }
1402
1420
  this._stopTimeLoop(id);
1403
- this._clearOverlay(id);
1421
+ this._detachOverlay(id);
1404
1422
  this._sk._tracker.deactivateContent();
1405
1423
  }
1406
1424
  // --- Time loop ---
@@ -1582,14 +1600,20 @@ class FeedManager {
1582
1600
  }
1583
1601
  }
1584
1602
  /** Clear the overlay container's DOM and unsubscribe tracked listeners. */
1585
- _clearOverlay(itemId) {
1603
+ /** Unsubscribe overlay event listeners without clearing DOM. */
1604
+ _detachOverlay(itemId) {
1586
1605
  const entry = this._overlayContainers.get(itemId);
1587
1606
  if (!entry) return;
1588
1607
  if (entry.unsub) {
1589
1608
  entry.unsub();
1590
1609
  entry.unsub = null;
1591
1610
  }
1592
- entry.el.innerHTML = "";
1611
+ }
1612
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
1613
+ _clearOverlay(itemId) {
1614
+ this._detachOverlay(itemId);
1615
+ const entry = this._overlayContainers.get(itemId);
1616
+ if (entry) entry.el.innerHTML = "";
1593
1617
  }
1594
1618
  }
1595
1619
  class EmbeddedFeedManager {
@@ -1638,6 +1662,7 @@ class EmbeddedFeedManager {
1638
1662
  this._setupScrollEnd();
1639
1663
  this._setupVisibilityHandler();
1640
1664
  this._setupCellHeightObserver();
1665
+ this._setupDebugPanel();
1641
1666
  if (startIndex > 0 && startIndex < items.length) {
1642
1667
  const targetEl = this.itemEls.get(items[startIndex].id);
1643
1668
  if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
@@ -1660,6 +1685,11 @@ class EmbeddedFeedManager {
1660
1685
  this._resizeObserver = null;
1661
1686
  }
1662
1687
  if (this._visHandler) document.removeEventListener("visibilitychange", this._visHandler);
1688
+ if (this._debugKeyHandler) document.removeEventListener("keydown", this._debugKeyHandler);
1689
+ if (this._debugPanel) {
1690
+ this._debugPanel.remove();
1691
+ this._debugPanel = null;
1692
+ }
1663
1693
  this.container.innerHTML = "";
1664
1694
  this.itemEls.clear();
1665
1695
  this.items = [];
@@ -1703,7 +1733,7 @@ class EmbeddedFeedManager {
1703
1733
  }
1704
1734
  setMuted(muted) {
1705
1735
  this.isMuted = muted;
1706
- for (const [, player] of this.pool._players) {
1736
+ for (const [, player] of this.pool.assignments) {
1707
1737
  player.muted = muted;
1708
1738
  }
1709
1739
  this._pushPlayerState({ isMuted: muted });
@@ -1716,7 +1746,7 @@ class EmbeddedFeedManager {
1716
1746
  }
1717
1747
  setPlaybackRate(rate) {
1718
1748
  this.playbackRate = rate;
1719
- for (const [, player] of this.pool._players) {
1749
+ for (const [, player] of this.pool.assignments) {
1720
1750
  player.playbackRate = rate;
1721
1751
  }
1722
1752
  this._pushPlayerState({ playbackRate: rate });
@@ -1988,11 +2018,52 @@ class EmbeddedFeedManager {
1988
2018
  const p = this.pool.getPlayer(id);
1989
2019
  if (p) {
1990
2020
  p.pause();
1991
- p.style.opacity = "0";
1992
2021
  p._skRevealedFor = null;
1993
2022
  }
1994
2023
  this._stopTimeLoop(id);
1995
- this._clearOverlay(id);
2024
+ this._detachOverlay(id);
2025
+ }
2026
+ // --- Debug panel (toggle with 'D' key) ---
2027
+ _setupDebugPanel() {
2028
+ const panel = document.createElement("div");
2029
+ panel.id = "sk-debug-panel";
2030
+ 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;";
2031
+ document.body.appendChild(panel);
2032
+ this._debugPanel = panel;
2033
+ this._debugVisible = false;
2034
+ this._debugKeyHandler = (e) => {
2035
+ if (e.key === "d" || e.key === "D") {
2036
+ this._debugVisible = !this._debugVisible;
2037
+ panel.style.display = this._debugVisible ? "block" : "none";
2038
+ }
2039
+ };
2040
+ document.addEventListener("keydown", this._debugKeyHandler);
2041
+ }
2042
+ _updateDebugPanel() {
2043
+ if (!this._debugVisible || !this._debugPanel) return;
2044
+ const hls = this.pool.hlsInstances.get(this.activeItemId);
2045
+ if (!hls || !hls.levels || !hls.levels.length) {
2046
+ this._debugPanel.innerHTML = "HLS: no levels loaded";
2047
+ return;
2048
+ }
2049
+ const level = hls.levels[hls.currentLevel] || {};
2050
+ const bw = hls.bandwidthEstimate ? (hls.bandwidthEstimate / 1e6).toFixed(2) : "?";
2051
+ const lines = [
2052
+ `<b style="color:#fff">HLS Debug</b>`,
2053
+ `Level: ${hls.currentLevel} / ${hls.levels.length - 1}`,
2054
+ `Resolution: ${level.width || "?"}x${level.height || "?"}`,
2055
+ `Bitrate: ${level.bitrate ? (level.bitrate / 1e3).toFixed(0) + " kbps" : "?"}`,
2056
+ `BW estimate: ${bw} Mbps`,
2057
+ `Auto cap: ${hls.autoLevelCapping}`,
2058
+ `Next level: ${hls.nextLevel}`,
2059
+ `<span style="color:#888">Levels:</span>`
2060
+ ];
2061
+ for (let i = 0; i < hls.levels.length; i++) {
2062
+ const l = hls.levels[i];
2063
+ const marker = i === hls.currentLevel ? ' <b style="color:#0ff">◀</b>' : "";
2064
+ lines.push(` ${i}: ${l.width}x${l.height} @ ${(l.bitrate / 1e3).toFixed(0)}k${marker}`);
2065
+ }
2066
+ this._debugPanel.innerHTML = lines.join("<br>");
1996
2067
  }
1997
2068
  // --- Time loop (state emission only, no DOM updates) ---
1998
2069
  _startTimeLoop(id, el, player) {
@@ -2000,6 +2071,7 @@ class EmbeddedFeedManager {
2000
2071
  const tick = () => {
2001
2072
  if (this._destroyed) return;
2002
2073
  if (!player || player.paused) {
2074
+ this._updateDebugPanel();
2003
2075
  this.rafIds.set(id, requestAnimationFrame(tick));
2004
2076
  return;
2005
2077
  }
@@ -2025,6 +2097,7 @@ class EmbeddedFeedManager {
2025
2097
  player.play().catch(() => {
2026
2098
  });
2027
2099
  }
2100
+ this._updateDebugPanel();
2028
2101
  this.rafIds.set(id, requestAnimationFrame(tick));
2029
2102
  };
2030
2103
  this.rafIds.set(id, requestAnimationFrame(tick));
@@ -2191,14 +2264,20 @@ class EmbeddedFeedManager {
2191
2264
  } catch (e) {
2192
2265
  }
2193
2266
  }
2194
- _clearOverlay(itemId) {
2267
+ /** Unsubscribe overlay event listeners without clearing DOM. */
2268
+ _detachOverlay(itemId) {
2195
2269
  const entry = this._overlayContainers.get(itemId);
2196
2270
  if (!entry) return;
2197
2271
  if (entry.unsub) {
2198
2272
  entry.unsub();
2199
2273
  entry.unsub = null;
2200
2274
  }
2201
- entry.el.innerHTML = "";
2275
+ }
2276
+ /** Unsubscribe and clear overlay DOM (used on destroy). */
2277
+ _clearOverlay(itemId) {
2278
+ this._detachOverlay(itemId);
2279
+ const entry = this._overlayContainers.get(itemId);
2280
+ if (entry) entry.el.innerHTML = "";
2202
2281
  }
2203
2282
  }
2204
2283
  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>';
@@ -2548,6 +2627,7 @@ class PlayerFeedView {
2548
2627
  this._isOpen = true;
2549
2628
  this._onClose = onClose;
2550
2629
  this._openItemId = item.id;
2630
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2551
2631
  const sourceRect = sourceEl.getBoundingClientRect();
2552
2632
  const sourceRadius = parseFloat(getComputedStyle(sourceEl).borderRadius) || 12;
2553
2633
  this.overlayEl.classList.add("skp-active");
@@ -2592,6 +2672,21 @@ class PlayerFeedView {
2592
2672
  feedEl.style.transform = "none";
2593
2673
  feedEl.style.borderRadius = `${targetRadius}px`;
2594
2674
  });
2675
+ this.feedManager.startObserver();
2676
+ this.feedManager._activateItem(item.id);
2677
+ this.overlayEl.classList.add("skp-feed-ready");
2678
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2679
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skp-feed-close, .sk-sidebar")];
2680
+ if (itemOverlay) chromeEls.push(itemOverlay);
2681
+ chromeEls.forEach((el) => {
2682
+ el.style.opacity = "0";
2683
+ el.style.transition = "none";
2684
+ });
2685
+ this.overlayEl.getBoundingClientRect();
2686
+ chromeEls.forEach((el) => {
2687
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2688
+ el.style.opacity = "1";
2689
+ });
2595
2690
  await this._waitForTransitionEnd(feedEl, 450);
2596
2691
  feedEl.classList.remove("skp-flip-animating");
2597
2692
  feedEl.style.transformOrigin = "";
@@ -2599,9 +2694,10 @@ class PlayerFeedView {
2599
2694
  feedEl.style.scrollSnapType = "";
2600
2695
  feedEl.style.transform = "";
2601
2696
  feedEl.style.borderRadius = "";
2602
- this.feedManager.startObserver();
2603
- this.feedManager._activateItem(item.id);
2604
- this.overlayEl.classList.add("skp-feed-ready");
2697
+ chromeEls.forEach((el) => {
2698
+ el.style.opacity = "";
2699
+ el.style.transition = "";
2700
+ });
2605
2701
  this._escHandler = (e) => {
2606
2702
  if (e.key === "Escape") this.close();
2607
2703
  };
@@ -2614,33 +2710,44 @@ class PlayerFeedView {
2614
2710
  const feedWrapper = feedEl.parentNode;
2615
2711
  const feedRect = feedEl.getBoundingClientRect();
2616
2712
  const activeItemId = this.feedManager?.activeItemId;
2617
- let transferBack = false, transferVideo = null, transferHls = null;
2618
- if (activeItemId === this._openItemId) {
2619
- const ejected = this.feedManager?.pool.ejectPlayer(activeItemId);
2620
- if (ejected) {
2621
- transferBack = true;
2622
- transferVideo = ejected.video;
2623
- transferHls = ejected.hls;
2624
- }
2625
- }
2713
+ const canTransfer = activeItemId === this._openItemId;
2626
2714
  let targetRect = null;
2627
2715
  if (this._onClose) targetRect = this._onClose("getSourceRect");
2628
2716
  if (feedRect && targetRect) {
2629
- if (this.feedManager?.activeItemId && !transferBack) {
2717
+ if (this.feedManager?.activeItemId && !canTransfer) {
2630
2718
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2631
2719
  }
2720
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2721
+ if (targetItemId && this.feedManager) {
2722
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2723
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2724
+ }
2725
+ const savedScrollTop = feedEl.scrollTop;
2632
2726
  feedEl.style.position = "fixed";
2633
2727
  feedEl.style.left = `${feedRect.left}px`;
2634
2728
  feedEl.style.top = `${feedRect.top}px`;
2635
2729
  feedEl.style.width = `${feedRect.width}px`;
2636
2730
  feedEl.style.height = `${feedRect.height}px`;
2637
- feedEl.style.zIndex = "9999";
2731
+ feedEl.style.zIndex = String(getComputedStyle(this.overlayEl).zIndex || 9999);
2638
2732
  feedEl.style.margin = "0";
2639
2733
  feedEl.style.flex = "none";
2640
2734
  feedEl.style.aspectRatio = "unset";
2641
2735
  feedEl.style.maxHeight = "none";
2642
2736
  document.body.appendChild(feedEl);
2737
+ feedEl.scrollTop = savedScrollTop;
2643
2738
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2739
+ let thumbOverlay = null;
2740
+ if (this._openThumbnailUrl) {
2741
+ const crossfadeItemId = canTransfer ? this._openItemId : activeItemId;
2742
+ const crossfadeEl = crossfadeItemId && this.feedManager?.itemEls.get(crossfadeItemId);
2743
+ const videoContainer = crossfadeEl?.querySelector('[data-ref="videoContainer"]');
2744
+ if (videoContainer) {
2745
+ thumbOverlay = document.createElement("img");
2746
+ thumbOverlay.src = this._openThumbnailUrl;
2747
+ 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;";
2748
+ videoContainer.appendChild(thumbOverlay);
2749
+ }
2750
+ }
2644
2751
  const scaleX = targetRect.width / feedRect.width;
2645
2752
  const scaleY = targetRect.height / feedRect.height;
2646
2753
  const translateX = targetRect.left - feedRect.left;
@@ -2650,29 +2757,58 @@ class PlayerFeedView {
2650
2757
  feedEl.style.transformOrigin = "0 0";
2651
2758
  feedEl.style.overflow = "hidden";
2652
2759
  feedEl.style.scrollSnapType = "none";
2760
+ feedEl.style.pointerEvents = "none";
2653
2761
  feedEl.getBoundingClientRect();
2654
2762
  feedEl.classList.add("skp-flip-animating");
2655
2763
  requestAnimationFrame(() => {
2656
2764
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2657
2765
  feedEl.style.borderRadius = `${endRadius}px`;
2766
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2658
2767
  });
2659
2768
  await this._waitForTransitionEnd(feedEl, 450);
2769
+ if (thumbOverlay) thumbOverlay.remove();
2770
+ let transferVideo = null;
2771
+ let transferHls = null;
2772
+ if (canTransfer) {
2773
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2774
+ if (ejected) {
2775
+ transferVideo = ejected.video;
2776
+ transferHls = ejected.hls;
2777
+ }
2778
+ }
2660
2779
  feedEl.style.visibility = "hidden";
2661
2780
  feedEl.classList.remove("skp-flip-animating");
2662
- ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2781
+ ["position", "left", "top", "width", "height", "zIndex", "margin", "flex", "aspectRatio", "maxHeight", "transformOrigin", "overflow", "scrollSnapType", "pointerEvents", "transform", "borderRadius"].forEach((p) => feedEl.style[p] = "");
2663
2782
  const sidebarEl = feedWrapper.querySelector(".sk-sidebar");
2664
2783
  feedWrapper.insertBefore(feedEl, sidebarEl);
2665
2784
  feedEl.style.visibility = "";
2785
+ if (this._onClose) {
2786
+ this._onClose("closed", {
2787
+ transferVideo,
2788
+ transferHls,
2789
+ transferItemId: transferVideo ? this._openItemId : null
2790
+ });
2791
+ this._onClose = null;
2792
+ }
2666
2793
  } else {
2667
2794
  this.overlayEl.classList.remove("skp-active", "skp-feed-ready");
2668
- }
2669
- if (this._onClose) {
2670
- this._onClose("closed", {
2671
- transferVideo: transferBack ? transferVideo : null,
2672
- transferHls: transferBack ? transferHls : null,
2673
- transferItemId: transferBack ? this._openItemId : null
2674
- });
2675
- this._onClose = null;
2795
+ if (this._onClose) {
2796
+ let transferVideo = null;
2797
+ let transferHls = null;
2798
+ if (canTransfer) {
2799
+ const ejected = this.feedManager?.pool.ejectPlayer(this._openItemId);
2800
+ if (ejected) {
2801
+ transferVideo = ejected.video;
2802
+ transferHls = ejected.hls;
2803
+ }
2804
+ }
2805
+ this._onClose("closed", {
2806
+ transferVideo,
2807
+ transferHls,
2808
+ transferItemId: transferVideo ? this._openItemId : null
2809
+ });
2810
+ this._onClose = null;
2811
+ }
2676
2812
  }
2677
2813
  if (this.feedManager) {
2678
2814
  this.feedManager.destroy();
@@ -2724,6 +2860,7 @@ class FeedView {
2724
2860
  this._onClose = onClose;
2725
2861
  this._openSlotIndex = startIndex;
2726
2862
  this._openItemId = item.id;
2863
+ this._openThumbnailUrl = item.thumbnailUrl || null;
2727
2864
  const slotRect = slotEl.getBoundingClientRect();
2728
2865
  const slotRadius = parseFloat(getComputedStyle(slotEl).borderRadius) || 12;
2729
2866
  if (_isPreview) {
@@ -2770,6 +2907,21 @@ class FeedView {
2770
2907
  feedEl.style.transform = "none";
2771
2908
  feedEl.style.borderRadius = `${targetRadius}px`;
2772
2909
  });
2910
+ this.feedManager.startObserver();
2911
+ this.feedManager._activateItem(item.id);
2912
+ this.overlayEl.classList.add("skw-feed-ready");
2913
+ const itemOverlay = feedItemEl?.querySelector('[data-ref="overlay"]');
2914
+ const chromeEls = [...this.overlayEl.querySelectorAll(".skw-feed-close, .sk-sidebar")];
2915
+ if (itemOverlay) chromeEls.push(itemOverlay);
2916
+ chromeEls.forEach((el) => {
2917
+ el.style.opacity = "0";
2918
+ el.style.transition = "none";
2919
+ });
2920
+ this.overlayEl.getBoundingClientRect();
2921
+ chromeEls.forEach((el) => {
2922
+ el.style.transition = "opacity .35s cubic-bezier(.32,.72,0,1)";
2923
+ el.style.opacity = "1";
2924
+ });
2773
2925
  await this._waitForTransitionEnd(feedEl, 450);
2774
2926
  feedEl.classList.remove("skw-flip-animating");
2775
2927
  feedEl.style.transformOrigin = "";
@@ -2777,9 +2929,10 @@ class FeedView {
2777
2929
  feedEl.style.scrollSnapType = "";
2778
2930
  feedEl.style.transform = "";
2779
2931
  feedEl.style.borderRadius = "";
2780
- this.feedManager.startObserver();
2781
- this.feedManager._activateItem(item.id);
2782
- this.overlayEl.classList.add("skw-feed-ready");
2932
+ chromeEls.forEach((el) => {
2933
+ el.style.opacity = "";
2934
+ el.style.transition = "";
2935
+ });
2783
2936
  this._escHandler = (e) => {
2784
2937
  if (e.key === "Escape") this.close();
2785
2938
  };
@@ -2806,6 +2959,12 @@ class FeedView {
2806
2959
  if (this.feedManager?.activeItemId && !canTransfer) {
2807
2960
  this.feedManager._deactivateItem(this.feedManager.activeItemId);
2808
2961
  }
2962
+ const targetItemId = canTransfer ? this._openItemId : activeItemId;
2963
+ if (targetItemId && this.feedManager) {
2964
+ const targetEl = this.feedManager.itemEls.get(targetItemId);
2965
+ if (targetEl) targetEl.scrollIntoView({ behavior: "instant", block: "start" });
2966
+ }
2967
+ const savedScrollTop = feedEl.scrollTop;
2809
2968
  feedEl.style.position = "fixed";
2810
2969
  feedEl.style.left = `${feedRect.left}px`;
2811
2970
  feedEl.style.top = `${feedRect.top}px`;
@@ -2817,7 +2976,20 @@ class FeedView {
2817
2976
  feedEl.style.aspectRatio = "unset";
2818
2977
  feedEl.style.maxHeight = "none";
2819
2978
  document.body.appendChild(feedEl);
2979
+ feedEl.scrollTop = savedScrollTop;
2820
2980
  this.overlayEl.classList.remove("skw-active", "skw-feed-ready");
2981
+ let thumbOverlay = null;
2982
+ if (this._openThumbnailUrl) {
2983
+ const targetItemId2 = canTransfer ? this._openItemId : activeItemId;
2984
+ const targetEl = targetItemId2 && this.feedManager?.itemEls.get(targetItemId2);
2985
+ const videoContainer = targetEl?.querySelector('[data-ref="videoContainer"]');
2986
+ if (videoContainer) {
2987
+ thumbOverlay = document.createElement("img");
2988
+ thumbOverlay.src = this._openThumbnailUrl;
2989
+ 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;";
2990
+ videoContainer.appendChild(thumbOverlay);
2991
+ }
2992
+ }
2821
2993
  const slotRadius = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--skw-radius").trim()) || 12;
2822
2994
  const scaleX = targetSlotRect.width / feedRect.width;
2823
2995
  const scaleY = targetSlotRect.height / feedRect.height;
@@ -2827,13 +2999,16 @@ class FeedView {
2827
2999
  feedEl.style.transformOrigin = "0 0";
2828
3000
  feedEl.style.overflow = "hidden";
2829
3001
  feedEl.style.scrollSnapType = "none";
3002
+ feedEl.style.pointerEvents = "none";
2830
3003
  feedEl.getBoundingClientRect();
2831
3004
  feedEl.classList.add("skw-flip-animating");
2832
3005
  requestAnimationFrame(() => {
2833
3006
  feedEl.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
2834
3007
  feedEl.style.borderRadius = `${endRadius}px`;
3008
+ if (thumbOverlay) thumbOverlay.style.opacity = "1";
2835
3009
  });
2836
3010
  await this._waitForTransitionEnd(feedEl, 450);
3011
+ if (thumbOverlay) thumbOverlay.remove();
2837
3012
  let transferVideo = null;
2838
3013
  let transferHls = null;
2839
3014
  if (canTransfer) {
@@ -2858,6 +3033,7 @@ class FeedView {
2858
3033
  feedEl.style.transformOrigin = "";
2859
3034
  feedEl.style.overflow = "";
2860
3035
  feedEl.style.scrollSnapType = "";
3036
+ feedEl.style.pointerEvents = "";
2861
3037
  feedEl.style.transform = "";
2862
3038
  feedEl.style.borderRadius = "";
2863
3039
  feedWrapper.insertBefore(feedEl, feedWrapper.firstChild);
@@ -2987,11 +3163,20 @@ class WidgetPlayerPool {
2987
3163
  }
2988
3164
  promoteToActive(itemId) {
2989
3165
  const hls = this.hlsInstances.get(itemId);
2990
- if (hls) {
2991
- hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
2992
- hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
2993
- hls.autoLevelCapping = -1;
2994
- hls.nextAutoLevel = -1;
3166
+ if (!hls) return;
3167
+ hls.config.maxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxBufferLength;
3168
+ hls.config.maxMaxBufferLength = WidgetPlayerPool.ACTIVE_HLS_CONFIG.maxMaxBufferLength;
3169
+ hls.autoLevelCapping = -1;
3170
+ const top = hls.levels ? hls.levels.length - 1 : -1;
3171
+ if (top > 0 && hls.currentLevel < top) {
3172
+ hls.currentLevel = top;
3173
+ const player = this.assignments.get(itemId);
3174
+ if (player) {
3175
+ player.currentTime = player.currentTime;
3176
+ }
3177
+ setTimeout(() => {
3178
+ if (!hls.destroyed) hls.currentLevel = -1;
3179
+ }, 4e3);
2995
3180
  }
2996
3181
  }
2997
3182
  _destroyHls(itemId) {
@@ -3596,7 +3781,7 @@ const CSS = `
3596
3781
 
3597
3782
  /* Video container */
3598
3783
  .sk-video-container{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background-size:cover;background-position:center}
3599
- .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .15s ease}
3784
+ .sk-video-container video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .35s ease}
3600
3785
 
3601
3786
  /* Tap zone */
3602
3787
  .sk-tap-zone{position:absolute;inset:0;z-index:3;cursor:pointer}
@@ -3612,7 +3797,7 @@ const CSS = `
3612
3797
 
3613
3798
  /* Widget slot */
3614
3799
  .skw-slot{position:relative;overflow:hidden;cursor:pointer}
3615
- .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .2s ease}
3800
+ .skw-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .4s ease}
3616
3801
  .skw-slot-thumb{position:absolute;inset:0;background-size:cover;background-position:center}
3617
3802
  .skw-slot-overlay{position:absolute;inset:0;z-index:4;pointer-events:none}
3618
3803
  .skw-slot-overlay>*{pointer-events:auto}