import { useEffect, useRef, useState } from "react" import { createFileRoute, Link } from "@tanstack/react-router" import { getStreamByUsername, type StreamPageData } from "@/lib/stream/db" import { VideoPlayer } from "@/components/VideoPlayer" import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer" import { WebRTCPlayer } from "@/components/WebRTCPlayer" import { resolveStreamPlayback } from "@/lib/stream/playback" import { JazzProvider } from "@/lib/jazz/provider" import { ViewerCount } from "@/components/ViewerCount" import { CommentBox } from "@/components/CommentBox" import { getSpotifyNowPlaying, type SpotifyNowPlayingResponse, } from "@/lib/spotify/now-playing" import { getStreamStatus } from "@/lib/stream/status" import { authClient } from "@/lib/auth-client" export const Route = createFileRoute("/$username")({ ssr: false, component: StreamPage, }) // Cloudflare Stream HLS URL const HLS_URL = "https://customer-xctsztqzu046isdc.cloudflarestream.com/bb7858eafc85de6c92963f3817477b5d/manifest/video.m3u8" const NIKIV_PLAYBACK = resolveStreamPlayback({ hlsUrl: HLS_URL, webrtcUrl: null }) const READY_PULSE_MS = 1200 // Hardcoded user for nikiv const NIKIV_DATA: StreamPageData = { user: { id: "nikiv", name: "Nikita", username: "nikiv", image: null, }, stream: { id: "nikiv-stream", title: "Live Coding", description: "Building in public", is_live: false, // Set to true when actually streaming viewer_count: 0, hls_url: HLS_URL, webrtc_url: null, playback: NIKIV_PLAYBACK, thumbnail_url: null, started_at: null, }, } // Free preview duration in milliseconds (5 minutes) const FREE_PREVIEW_MS = 5 * 60 * 1000 const STORAGE_KEY = "linsa_stream_watch_time" function getWatchTime(): number { if (typeof window === "undefined") return 0 const stored = localStorage.getItem(STORAGE_KEY) if (!stored) return 0 try { const data = JSON.parse(stored) // Reset if older than 24 hours if (Date.now() - data.startedAt > 24 * 60 * 60 * 1000) { localStorage.removeItem(STORAGE_KEY) return 0 } return data.watchTime || 0 } catch { return 0 } } function saveWatchTime(watchTime: number, startedAt: number) { if (typeof window === "undefined") return localStorage.setItem(STORAGE_KEY, JSON.stringify({ watchTime, startedAt })) } function StreamPage() { const { username } = Route.useParams() const { data: session, isPending: sessionLoading } = authClient.useSession() 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 [hlsLive, setHlsLive] = useState(null) const [nowPlaying, setNowPlaying] = useState( null, ) const [nowPlayingLoading, setNowPlayingLoading] = useState(false) const [nowPlayingError, setNowPlayingError] = useState(false) const [streamLive, setStreamLive] = useState(false) const [showReadyPulse, setShowReadyPulse] = useState(false) const readyPulseTimeoutRef = useRef | null>(null) // Free preview tracking const [watchTime, setWatchTime] = useState(0) const [previewExpired, setPreviewExpired] = useState(false) const watchStartRef = useRef(null) const isAuthenticated = !sessionLoading && !!session?.user // Track watch time for unauthenticated users useEffect(() => { if (isAuthenticated || sessionLoading) return // Initialize from localStorage const savedTime = getWatchTime() setWatchTime(savedTime) if (savedTime >= FREE_PREVIEW_MS) { setPreviewExpired(true) return } watchStartRef.current = Date.now() const startedAt = Date.now() - savedTime const interval = setInterval(() => { const elapsed = Date.now() - (watchStartRef.current || Date.now()) + savedTime setWatchTime(elapsed) saveWatchTime(elapsed, startedAt) if (elapsed >= FREE_PREVIEW_MS) { setPreviewExpired(true) clearInterval(interval) } }, 1000) return () => { clearInterval(interval) if (watchStartRef.current) { const elapsed = Date.now() - watchStartRef.current + savedTime saveWatchTime(elapsed, startedAt) } } }, [isAuthenticated, sessionLoading]) useEffect(() => { let isActive = true const setReadySafe = (ready: boolean) => { if (isActive) { setStreamReady(ready) } } const setDataSafe = (next: StreamPageData | null) => { if (isActive) { setData(next) } } const setLoadingSafe = (next: boolean) => { if (isActive) { setLoading(next) } } const setErrorSafe = (next: string | null) => { if (isActive) { setError(next) } } const setWebRtcFailedSafe = (next: boolean) => { if (isActive) { setWebRtcFailed(next) } } setReadySafe(false) setWebRtcFailedSafe(false) // Special handling for nikiv - hardcoded stream if (username === "nikiv") { setDataSafe(NIKIV_DATA) setLoadingSafe(false) return () => { isActive = false } } const loadData = async () => { setLoadingSafe(true) setErrorSafe(null) try { const result = await getStreamByUsername(username) setDataSafe(result) } catch (err) { setErrorSafe("Failed to load stream") console.error(err) } finally { setLoadingSafe(false) } } loadData() return () => { isActive = false } }, [username]) // Poll stream status for nikiv from nikiv.dev/api/stream-status useEffect(() => { if (username !== "nikiv") { return } let isActive = true const fetchStatus = async () => { const status = await getStreamStatus() console.log("[Stream Status] nikiv.dev/api/stream-status:", status) if (isActive) { setStreamLive(status.isLive) } } // Fetch immediately fetchStatus() // Poll every 10 seconds const interval = setInterval(fetchStatus, 10000) return () => { isActive = false clearInterval(interval) } }, [username]) useEffect(() => { if (readyPulseTimeoutRef.current) { clearTimeout(readyPulseTimeoutRef.current) readyPulseTimeoutRef.current = null } if (!streamReady) { setShowReadyPulse(false) return } setShowReadyPulse(true) readyPulseTimeoutRef.current = setTimeout(() => { setShowReadyPulse(false) readyPulseTimeoutRef.current = null }, READY_PULSE_MS) return () => { if (readyPulseTimeoutRef.current) { clearTimeout(readyPulseTimeoutRef.current) readyPulseTimeoutRef.current = null } } }, [streamReady]) 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 const isHlsPlaylistLive = (manifest: string) => { const upper = manifest.toUpperCase() const hasEndlist = upper.includes("#EXT-X-ENDLIST") const isVod = upper.includes("#EXT-X-PLAYLIST-TYPE:VOD") const hasSegments = upper.includes("#EXTINF") || upper.includes("#EXT-X-PART") // Also check for #EXTM3U which is the start of any valid HLS manifest const isValidManifest = upper.includes("#EXTM3U") return isValidManifest && !hasEndlist && !isVod && hasSegments } // For nikiv, use server-side API to check HLS (avoids CORS) useEffect(() => { if (username !== "nikiv") return 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) setHlsLive(true) } else { setStreamReady(false) setHlsLive(false) } } catch (err) { console.error("[HLS Check] API error:", err) if (isActive) { setStreamReady(false) setHlsLive(false) } } } // Initial check setStreamReady(false) setHlsLive(null) checkHlsViaApi() // Poll every 5 seconds to detect when stream goes live const interval = setInterval(checkHlsViaApi, 5000) return () => { isActive = false clearInterval(interval) } }, [username]) // For non-nikiv users, use direct HLS check useEffect(() => { let isActive = true if (username === "nikiv" || !activePlayback || activePlayback.type !== "hls") { return () => { isActive = false } } const checkHlsLive = async () => { console.log("[HLS Check] Fetching manifest:", activePlayback.url) try { const res = await fetch(activePlayback.url, { cache: "no-store", mode: "cors", }) if (!isActive) return console.log("[HLS Check] Response status:", res.status, res.ok) if (!res.ok) { setStreamReady(false) setHlsLive(false) return } const manifest = await res.text() 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) setHlsLive(false) } } } // Initial check setStreamReady(false) setHlsLive(null) checkHlsLive() // Poll every 5 seconds to detect when stream goes live const interval = setInterval(checkHlsLive, 5000) return () => { isActive = false clearInterval(interval) } }, [ username, activePlayback?.type, activePlayback?.type === "hls" ? activePlayback.url : null, ]) useEffect(() => { let isActive = true if (!stream?.hls_url || activePlayback?.type === "hls") { return () => { isActive = false } } setHlsLive(null) fetch(stream.hls_url) .then(async (res) => { if (!isActive) return if (!res.ok) { setHlsLive(false) return } const manifest = await res.text() if (!isActive) return setHlsLive(isHlsPlaylistLive(manifest)) }) .catch(() => { if (isActive) { setHlsLive(false) } }) return () => { isActive = false } }, [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 other users, use stream?.is_live from the database const isLiveStatus = username === "nikiv" ? (hlsLive === true || (hlsLive === null && streamLive)) : 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]) useEffect(() => { if (!shouldFetchSpotify) { setNowPlaying(null) setNowPlayingLoading(false) setNowPlayingError(false) return } let isActive = true const fetchNowPlaying = async (showLoading: boolean) => { if (showLoading) { setNowPlayingLoading(true) } try { const response = await getSpotifyNowPlaying() if (!isActive) return setNowPlaying(response) setNowPlayingError(false) } catch (err) { if (!isActive) return console.error("Failed to load Spotify now playing", err) setNowPlayingError(true) } finally { if (isActive && showLoading) { setNowPlayingLoading(false) } } } fetchNowPlaying(true) const interval = setInterval(() => fetchNowPlaying(false), 30000) return () => { isActive = false clearInterval(interval) } }, [shouldFetchSpotify]) // Format remaining time const remainingMs = Math.max(0, FREE_PREVIEW_MS - watchTime) const remainingMin = Math.floor(remainingMs / 60000) const remainingSec = Math.floor((remainingMs % 60000) / 1000) const remainingFormatted = `${remainingMin}:${remainingSec.toString().padStart(2, "0")}` // Auth gate - show preview for 5 min, then require login if (sessionLoading) { return (
Loading...
) } // Show auth wall when preview expires for unauthenticated users if (!isAuthenticated && previewExpired) { return (

Free preview ended

Sign in to continue watching this stream

Sign in to continue
) } if (loading) { return (
Loading...
) } if (error) { return (

Error

{error}

) } if (!data) { return (

User not found

This username doesn't exist or hasn't set up streaming.

) } 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(", ") : null const nowPlayingText = nowPlayingTrack ? nowPlayingArtists ? `${nowPlayingArtists} — ${nowPlayingTrack.title}` : nowPlayingTrack.title : null return (
{/* Main content area */}
{/* Free preview countdown banner for unauthenticated users */} {!isAuthenticated && !previewExpired && (
Free preview: {remainingFormatted} remaining Sign in for unlimited access
)} {/* Viewer count overlay */}
{isActuallyLive && activePlayback && showPlayer ? ( activePlayback.type === "webrtc" ? (
setStreamReady(true)} onError={() => { setWebRtcFailed(true) setStreamReady(!fallbackPlayback) }} /> {!streamReady && (
🟡
)} {showReadyPulse && (
🔴
)}
) : activePlayback.type === "cloudflare" ? (
setStreamReady(true)} /> {!streamReady && (
🟡
)} {showReadyPulse && (
🔴
)}
) : (
{showReadyPulse && (
🔴
)}
) ) : 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 */}
) }