From 31412416260b3cea1c50137471d2a1c9b4b9bc74 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 24 Dec 2025 19:06:58 -0800 Subject: [PATCH] Improve VideoPlayer to notify when ready and update StreamPage to handle player readiness and connection state --- packages/web/src/components/VideoPlayer.tsx | 27 +- packages/web/src/routes/$username.tsx | 376 ++++++++++---------- packages/web/src/routes/api/check-hls.ts | 2 +- 3 files changed, 213 insertions(+), 192 deletions(-) diff --git a/packages/web/src/components/VideoPlayer.tsx b/packages/web/src/components/VideoPlayer.tsx index a6177bb4..ee82780f 100644 --- a/packages/web/src/components/VideoPlayer.tsx +++ b/packages/web/src/components/VideoPlayer.tsx @@ -5,12 +5,14 @@ interface VideoPlayerProps { src: string autoPlay?: boolean muted?: boolean + onReady?: () => void } export function VideoPlayer({ src, autoPlay = true, muted = false, + onReady, }: VideoPlayerProps) { const containerRef = useRef(null) const videoRef = useRef(null) @@ -39,7 +41,17 @@ export function VideoPlayer({ // Check if native HLS is supported (Safari) if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = src - if (autoPlay) video.play().catch(() => setIsPlaying(false)) + if (autoPlay) { + video.play() + .then(() => { + setIsPlaying(true) + onReady?.() + }) + .catch(() => setIsPlaying(false)) + } else { + // Even if not autoplay, notify ready when we can play + video.addEventListener("canplay", () => onReady?.(), { once: true }) + } return } @@ -56,7 +68,16 @@ export function VideoPlayer({ hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { - if (autoPlay) video.play().catch(() => setIsPlaying(false)) + if (autoPlay) { + video.play() + .then(() => { + setIsPlaying(true) + onReady?.() + }) + .catch(() => setIsPlaying(false)) + } else { + onReady?.() + } }) hls.on(Hls.Events.ERROR, (_, data) => { @@ -87,7 +108,7 @@ export function VideoPlayer({ } else { setError("HLS playback not supported in this browser") } - }, [src, autoPlay]) + }, [src, autoPlay, onReady]) useEffect(() => { return () => { diff --git a/packages/web/src/routes/$username.tsx b/packages/web/src/routes/$username.tsx index f757ab5f..603e29a4 100644 --- a/packages/web/src/routes/$username.tsx +++ b/packages/web/src/routes/$username.tsx @@ -14,6 +14,7 @@ import { } from "@/lib/spotify/now-playing" import { getStreamStatus } from "@/lib/stream/status" import { authClient } from "@/lib/auth-client" +import { MessageCircle, LogIn, X } from "lucide-react" export const Route = createFileRoute("/$username")({ ssr: false, @@ -21,7 +22,7 @@ export const Route = createFileRoute("/$username")({ }) // Cloudflare Stream HLS URL -const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/bb7858eafc85de6c92963f3817477b5d/manifest/video.m3u8" +const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/1b0363e3f8d54ddc639dc85737f8c28a/manifest/video.m3u8" const NIKIV_PLAYBACK = resolveStreamPlayback({ hlsUrl: HLS_URL, webrtcUrl: null }) const READY_PULSE_MS = 1200 @@ -79,9 +80,9 @@ function StreamPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const [streamReady, setStreamReady] = useState(false) - const [webRtcFailed, setWebRtcFailed] = useState(false) + const [playerReady, setPlayerReady] = useState(false) const [hlsLive, setHlsLive] = useState(null) + const [isConnecting, setIsConnecting] = useState(false) const [nowPlaying, setNowPlaying] = useState( null, ) @@ -90,12 +91,16 @@ function StreamPage() { const [streamLive, setStreamLive] = useState(false) const [showReadyPulse, setShowReadyPulse] = useState(false) const readyPulseTimeoutRef = useRef | null>(null) + const hasConnectedOnce = useRef(false) // Free preview tracking const [watchTime, setWatchTime] = useState(0) const [previewExpired, setPreviewExpired] = useState(false) const watchStartRef = useRef(null) + // Mobile chat overlay + const [showMobileChat, setShowMobileChat] = useState(false) + const isAuthenticated = !sessionLoading && !!session?.user // Track watch time for unauthenticated users @@ -228,7 +233,7 @@ function StreamPage() { readyPulseTimeoutRef.current = null } - if (!streamReady) { + if (!playerReady) { setShowReadyPulse(false) return } @@ -245,17 +250,13 @@ function StreamPage() { readyPulseTimeoutRef.current = null } } - }, [streamReady]) + }, [playerReady]) const stream = data?.stream ?? null - const playback = stream?.playback ?? null - const fallbackPlayback = stream?.hls_url - ? { type: "hls", url: stream.hls_url } - : null - const activePlayback = - playback?.type === "webrtc" && webRtcFailed - ? fallbackPlayback ?? playback - : playback + // For nikiv, always use HLS directly (no WebRTC) + const activePlayback = username === "nikiv" + ? { type: "hls" as const, url: HLS_URL } + : stream?.playback ?? null const isHlsPlaylistLive = (manifest: string) => { const upper = manifest.toUpperCase() @@ -275,32 +276,31 @@ function StreamPage() { let isActive = true const checkHlsViaApi = async () => { - console.log("[HLS Check] Calling /api/check-hls...") try { const res = await fetch("/api/check-hls", { cache: "no-store" }) if (!isActive) return const data = await res.json() - console.log("[HLS Check] API response:", data) if (data.isLive) { - setStreamReady(true) + // Stream is live - set connecting state if first time + if (!hasConnectedOnce.current) { + setIsConnecting(true) + } setHlsLive(true) } else { - setStreamReady(false) - setHlsLive(false) - } - } catch (err) { - console.error("[HLS Check] API error:", err) - if (isActive) { - setStreamReady(false) - setHlsLive(false) + // Only set offline if we haven't connected yet + // This prevents flickering when HLS check temporarily fails + if (!hasConnectedOnce.current) { + setHlsLive(false) + } } + } catch { + // Silently ignore errors - don't change state on network issues } } // Initial check - setStreamReady(false) setHlsLive(null) checkHlsViaApi() @@ -315,15 +315,13 @@ function StreamPage() { // For non-nikiv users, use direct HLS check useEffect(() => { - let isActive = true if (username === "nikiv" || !activePlayback || activePlayback.type !== "hls") { - return () => { - isActive = false - } + return } + let isActive = true + const checkHlsLive = async () => { - console.log("[HLS Check] Fetching manifest:", activePlayback.url) try { const res = await fetch(activePlayback.url, { cache: "no-store", @@ -332,11 +330,10 @@ function StreamPage() { if (!isActive) return - console.log("[HLS Check] Response status:", res.status, res.ok) - if (!res.ok) { - setStreamReady(false) - setHlsLive(false) + if (!hasConnectedOnce.current) { + setHlsLive(false) + } return } @@ -344,28 +341,22 @@ function StreamPage() { if (!isActive) return const live = isHlsPlaylistLive(manifest) - console.log("[HLS Check] Manifest live check:", { - live, - manifestLength: manifest.length, - first200: manifest.slice(0, 200) - }) - setStreamReady(live) - setHlsLive(live) - } catch (err) { - console.error("[HLS Check] Fetch error:", err) - if (isActive) { - setStreamReady(false) + if (live) { + if (!hasConnectedOnce.current) { + setIsConnecting(true) + } + setHlsLive(true) + } else if (!hasConnectedOnce.current) { setHlsLive(false) } + } catch { + // Silently ignore fetch errors } } - // Initial check - setStreamReady(false) setHlsLive(null) checkHlsLive() - // Poll every 5 seconds to detect when stream goes live const interval = setInterval(checkHlsLive, 5000) return () => { @@ -409,30 +400,14 @@ function StreamPage() { } }, [activePlayback?.type, stream?.hls_url]) - // For nikiv, primarily use HLS live check from Cloudflare - // Fall back to streamLive status if HLS check hasn't completed + // For nikiv, use HLS live check from our API // For other users, use stream?.is_live from the database - const isLiveStatus = username === "nikiv" - ? (hlsLive === true || (hlsLive === null && streamLive)) + const isActuallyLive = username === "nikiv" + ? hlsLive === true : Boolean(stream?.is_live) - const isActuallyLive = isLiveStatus - const shouldFetchSpotify = username === "nikiv" && !isActuallyLive - // Debug logging for stream status - useEffect(() => { - if (username === "nikiv") { - console.log("[Stream Debug]", { - streamLive, - hlsLive, - isLiveStatus, - isActuallyLive, - streamReady, - activePlaybackType: activePlayback?.type, - webRtcFailed, - hlsUrl: stream?.hls_url, - }) - } - }, [username, streamLive, hlsLive, isLiveStatus, isActuallyLive, streamReady, activePlayback?.type, webRtcFailed, stream?.hls_url]) + // Only show Spotify when we know stream is offline (not during initial check) + const shouldFetchSpotify = username === "nikiv" && !isActuallyLive && hlsLive === false useEffect(() => { if (!shouldFetchSpotify) { @@ -453,9 +428,9 @@ function StreamPage() { if (!isActive) return setNowPlaying(response) setNowPlayingError(false) - } catch (err) { + } catch { if (!isActive) return - console.error("Failed to load Spotify now playing", err) + // Silently handle Spotify errors - it's not critical setNowPlayingError(true) } finally { if (isActive && showLoading) { @@ -540,10 +515,6 @@ function StreamPage() { ) } - const showPlayer = - activePlayback?.type === "cloudflare" || - activePlayback?.type === "webrtc" || - (activePlayback?.type === "hls" && streamReady) const nowPlayingTrack = nowPlaying?.track ?? null const nowPlayingArtists = nowPlayingTrack?.artists.length ? nowPlayingTrack.artists.join(", ") @@ -554,19 +525,29 @@ function StreamPage() { : nowPlayingTrack.title : null + // Callback when player is ready + const handlePlayerReady = () => { + hasConnectedOnce.current = true + setIsConnecting(false) + setPlayerReady(true) + } + + // Show loading state during initial check + const isChecking = hlsLive === null + return ( -
+
{/* Main content area */} -
- {/* Free preview countdown banner for unauthenticated users */} - {!isAuthenticated && !previewExpired && ( -
+
+ {/* Free preview countdown banner - hidden on mobile */} + {!isAuthenticated && !previewExpired && isActuallyLive && ( +
Free preview: {remainingFormatted} remaining Sign in for unlimited access @@ -574,47 +555,39 @@ function StreamPage() {
)} - {/* Viewer count overlay */} -
- -
+ {/* Viewer count overlay - hidden on mobile */} + {isActuallyLive && ( +
+ +
+ )} - {isActuallyLive && activePlayback && showPlayer ? ( - activePlayback.type === "webrtc" ? ( + {/* Loading state - checking if stream is live */} + {isChecking ? ( +
+
+
+
+

Checking stream status...

+
+ ) : isActuallyLive && activePlayback ? ( + /* Stream is live - show the player */
- setStreamReady(true)} - onError={() => { - setWebRtcFailed(true) - setStreamReady(!fallbackPlayback) - }} + onReady={handlePlayerReady} /> - {!streamReady && ( -
-
🟡
-
- )} - {showReadyPulse && ( -
-
🔴
-
- )} -
- ) : activePlayback.type === "cloudflare" ? ( -
- setStreamReady(true)} - /> - {!streamReady && ( -
-
🟡
+ {/* Loading overlay while connecting */} + {(isConnecting || !playerReady) && ( +
+
+
+
+

Connecting to stream...

)} + {/* Ready pulse */} {showReadyPulse && (
🔴
@@ -622,86 +595,113 @@ function StreamPage() { )}
) : ( -
- - {showReadyPulse && ( -
-
🔴
+ /* Stream is offline */ +
+ {shouldFetchSpotify ? ( +
+
+ + Offline +
+

+ Not live right now +

+
+ {nowPlayingLoading ? ( + Checking Spotify... + ) : nowPlaying?.isPlaying && nowPlayingTrack ? ( + + Currently playing{" "} + {nowPlayingTrack.url ? ( + + {nowPlayingText ?? "Spotify"} + + ) : ( + + {nowPlayingText ?? "Spotify"} + + )} + + ) : null} +
+ + + nikiv.dev + +
+ ) : ( + )}
- ) - ) : isActuallyLive && activePlayback ? ( -
-
🟡
-
- ) : ( -
- {shouldFetchSpotify ? ( -
-
- - Offline -
-

- Not live right now -

-
- {nowPlayingLoading ? ( - Checking Spotify... - ) : nowPlaying?.isPlaying && nowPlayingTrack ? ( - - Currently playing{" "} - {nowPlayingTrack.url ? ( - - {nowPlayingText ?? "Spotify"} - - ) : ( - - {nowPlayingText ?? "Spotify"} - - )} - - ) : null} -
- - - nikiv.dev - -
- ) : ( - - )} -
- )} + )}
- {/* Chat sidebar */} -
+ {/* Desktop Chat sidebar */} +
+ + {/* Mobile bottom bar */} +
+ {!isAuthenticated && ( + + + Sign In + + )} + +
+ + {/* Mobile chat overlay */} + {showMobileChat && ( +
+
+ Chat + +
+
+ +
+
+ )}
) diff --git a/packages/web/src/routes/api/check-hls.ts b/packages/web/src/routes/api/check-hls.ts index a14e6c97..45d30648 100644 --- a/packages/web/src/routes/api/check-hls.ts +++ b/packages/web/src/routes/api/check-hls.ts @@ -7,7 +7,7 @@ const json = (data: unknown, status = 200) => }) // Cloudflare Stream HLS URL -const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/bb7858eafc85de6c92963f3817477b5d/manifest/video.m3u8" +const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/1b0363e3f8d54ddc639dc85737f8c28a/manifest/video.m3u8" function isHlsPlaylistLive(manifest: string): boolean { const upper = manifest.toUpperCase()