diff --git a/src/screens/Playbacks/Playbacks.web.js b/src/screens/Playbacks/Playbacks.web.js index 2c4abd1..e7ce723 100644 --- a/src/screens/Playbacks/Playbacks.web.js +++ b/src/screens/Playbacks/Playbacks.web.js @@ -10,7 +10,6 @@ import PlaybackItem from "./components/PlaybackItem.web"; const Playbacks = () => { const { height: windowHeight } = useWindowDimensions(); const [activeIndex, setActiveIndex] = useState(0); - const carouselRef = useRef(null); const route = useRoute(); const focusProjectId = route?.params?.projectId || route?.params?.focusId || null; @@ -25,15 +24,132 @@ const Playbacks = () => { batchSize: 6, }); + const activePlayback = playbacks?.[activeIndex] || null; + const activeVideoUrl = activePlayback?.playbackUrl || null; + const backgroundVideoRef = useRef(null); + const lastActiveIndexRef = useRef(0); + const backgroundSeekPendingRef = useRef(false); + const listRef = useRef(null); + + useEffect(() => { + const total = playbacks?.length || 0; + if (total <= 0) { + if (activeIndex !== 0) { + lastActiveIndexRef.current = 0; + setActiveIndex(0); + } + return; + } + const maxIndex = total - 1; + if (activeIndex > maxIndex) { + lastActiveIndexRef.current = maxIndex; + setActiveIndex(maxIndex); + } + }, [playbacks?.length, activeIndex]); + + const handleActiveIndexChange = useCallback( + (rawIndex) => { + if (Number.isNaN(rawIndex)) return; + const total = playbacks?.length || 0; + const maxIndex = Math.max(0, total - 1); + const clamped = Math.max(0, Math.min(rawIndex, maxIndex)); + if (clamped === lastActiveIndexRef.current) return; + lastActiveIndexRef.current = clamped; + setActiveIndex((prev) => (prev === clamped ? prev : clamped)); + if (total > 0 && clamped >= Math.max(0, total - 6)) { + loadMore?.(); + } + }, + [playbacks?.length, loadMore] + ); + + const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => { + const bg = backgroundVideoRef.current; + if (!bg) return; + + if (typeof currentTime === "number" && Number.isFinite(currentTime)) { + const applyTime = () => { + backgroundSeekPendingRef.current = false; + const diff = Math.abs((bg.currentTime || 0) - currentTime); + if (diff > 0.25) { + try { + bg.currentTime = currentTime; + } catch (_e) {} + } + }; + + if (bg.readyState >= 1) applyTime(); + else if (!backgroundSeekPendingRef.current) { + backgroundSeekPendingRef.current = true; + const handler = () => applyTime(); + bg.addEventListener("loadeddata", handler, { once: true }); + } + } + + if (isPlaying === true) { + if (bg.paused) { + bg.play().catch(() => {}); + } + } else if (isPlaying === false) { + try { + if (!bg.paused) bg.pause(); + } catch (_e) {} + } + }, []); + + useEffect(() => { + const bg = backgroundVideoRef.current; + if (!bg) return; + if (!activeVideoUrl) { + backgroundSeekPendingRef.current = false; + try { + bg.pause(); + } catch (_e) {} + bg.removeAttribute?.("src"); + bg.load?.(); + return; + } + backgroundSeekPendingRef.current = false; + const setSrcIfNeeded = () => { + const attrSrc = bg.getAttribute("src"); + if (attrSrc !== activeVideoUrl) { + bg.setAttribute("src", activeVideoUrl); + try { + bg.load(); + } catch (_e) {} + } + }; + + const startPlayback = () => { + bg.play().catch(() => {}); + }; + + setSrcIfNeeded(); + + if (bg.readyState >= 2) { + startPlayback(); + return; + } + + const handleLoaded = () => { + startPlayback(); + }; + bg.addEventListener("loadeddata", handleLoaded, { once: true }); + return () => { + bg.removeEventListener("loadeddata", handleLoaded); + }; + }, [activeVideoUrl]); + const triedLoadMoreRef = useRef(0); useEffect(() => { if (!focusProjectId) return; const idx = playbacks.findIndex((p) => p?.id === focusProjectId); if (idx >= 0) { + lastActiveIndexRef.current = idx; setActiveIndex(idx); setTimeout(() => { try { - carouselRef.current?.scrollTo?.({ index: idx, animated: false }); + listRef.current?.scrollToIndex?.({ index: idx, animated: false }); } catch (_e) {} }, 50); } else if (loadMore && triedLoadMoreRef.current < 6) { @@ -43,22 +159,27 @@ const Playbacks = () => { }, [focusProjectId, playbacks, loadMore]); // === FlatList (one real page per item) === - const listRef = useRef(null); - // keep active index in sync with scroll const onMomentumScrollEnd = useCallback( (e) => { try { const y = e?.nativeEvent?.contentOffset?.y || 0; const idx = Math.round(y / windowHeight); - if (!Number.isNaN(idx)) - setActiveIndex( - Math.max(0, Math.min(idx, (playbacks?.length || 1) - 1)) - ); - if (idx >= (playbacks?.length || 0) - 6) loadMore?.(); + handleActiveIndexChange(idx); } catch (_e) {} }, - [windowHeight, playbacks?.length, loadMore] + [windowHeight, handleActiveIndexChange] + ); + + const onScroll = useCallback( + (e) => { + try { + const y = e?.nativeEvent?.contentOffset?.y || 0; + const idx = Math.round(y / windowHeight); + handleActiveIndexChange(idx); + } catch (_e) {} + }, + [windowHeight, handleActiveIndexChange] ); // programmatic jump to focus item @@ -66,6 +187,7 @@ const Playbacks = () => { if (!focusProjectId) return; const idx = playbacks.findIndex((p) => p?.id === focusProjectId); if (idx >= 0) { + lastActiveIndexRef.current = idx; setActiveIndex(idx); setTimeout(() => { try { @@ -85,8 +207,36 @@ const Playbacks = () => { ); return ( - + + {activeVideoUrl ? ( + + )} @@ -108,7 +259,8 @@ const Playbacks = () => { initialNumToRender={3} windowSize={5} removeClippedSubviews - scrollEventThrottle={32} + onScroll={onScroll} + scrollEventThrottle={16} /> @@ -121,5 +273,16 @@ const styles = StyleSheet.create({ screen: { flex: 1, overscrollBehavior: "none", + position: "relative", + }, + backgroundVideoWrapper: { + ...StyleSheet.absoluteFillObject, + overflow: "hidden", + zIndex: -1, + backgroundColor: "#060606", + }, + backgroundOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "rgba(2, 2, 2, 0.35)", }, }); diff --git a/src/screens/Playbacks/components/PlaybackItem.web.js b/src/screens/Playbacks/components/PlaybackItem.web.js index fddb2d9..a4095d1 100644 --- a/src/screens/Playbacks/components/PlaybackItem.web.js +++ b/src/screens/Playbacks/components/PlaybackItem.web.js @@ -32,12 +32,21 @@ import { FONT_FAMILY } from "../../../styles/Fonts"; import { size } from "../../../styles/Style"; import CommentsPanel from "./CommentsPanel.web"; +// Debug logging toggle for web playback +const DEBUG_PLAYBACK_WEB = true; + // NOTE: Web-only implementation that uses a native