import { useState, useEffect } from "react" import { createFileRoute } from "@tanstack/react-router" import { useAccount } from "jazz-tools/react" import { ViewerAccount, type StreamRecording, StreamRecordingList } from "@/lib/jazz/schema" import { StreamTimeline } from "@/components/StreamTimeline" import { Video, RefreshCw } from "lucide-react" import { co } from "jazz-tools" export const Route = createFileRoute("/streams")({ component: StreamsPage, ssr: false, }) function StreamsPage() { const me = useAccount(ViewerAccount) const [syncing, setSyncing] = useState(false) const [lastSync, setLastSync] = useState(null) const root = me.$isLoaded ? me.root : null const recordingsList = root?.$isLoaded ? root.streamRecordings : null // Auto-sync pending recordings from API every 5 seconds useEffect(() => { const interval = setInterval(() => { void syncPendingRecordings() }, 5000) return () => clearInterval(interval) }, [root]) const syncPendingRecordings = async () => { if (!root?.streamRecordings?.$isLoaded || syncing) return setSyncing(true) try { // Fetch pending recordings from stream-guard API const response = await fetch("/api/stream-recording") if (!response.ok) { console.error("[streams] Failed to fetch recordings") return } const data = (await response.json()) as { recordings: Array<{ streamId: string title: string startedAt: number endedAt?: number streamKey: string status: string chunks: Array<{ index: number; timestamp: number; size: number }> metadata?: { width?: number height?: number fps?: number bitrate?: number } }> } const pendingRecordings = data.recordings if (pendingRecordings.length === 0) { return } console.log(`[streams] Found ${pendingRecordings.length} recordings to sync`) // Get existing IDs to avoid duplicates const existingKeys = new Set( root.streamRecordings.$isLoaded ? [...root.streamRecordings].map((item) => item.streamKey) : [] ) // Process each recording for (const rec of pendingRecordings) { if (existingKeys.has(rec.streamKey)) { // Update existing recording const existing = [...root.streamRecordings].find( (r) => r.streamKey === rec.streamKey ) if (existing && rec.endedAt && !existing.endedAt) { // Mark as ended existing.endedAt = rec.endedAt existing.isLive = false existing.durationMs = rec.endedAt - rec.startedAt } continue } // Create new recording in Jazz try { // Create FileStream from chunks const fileStream = co.fileStream().create({ owner: me }) // Start the stream with metadata fileStream.start({ mimeType: "video/x-matroska", // .mkv format fileName: `${rec.title}.mkv`, totalSizeBytes: rec.chunks.reduce((sum, c) => sum + c.size, 0), }) // Fetch and push chunks for (const chunk of rec.chunks) { try { const chunkPath = `/Users/nikiv/fork-i/garden-co/jazz/glide-storage/stream-recordings/${rec.streamId}/chunk-${String(chunk.index).padStart(6, "0")}.bin` const chunkData = await fetch( `/api/stream-recording/chunk?path=${encodeURIComponent(chunkPath)}` ).then((r) => r.arrayBuffer()) fileStream.push(new Uint8Array(chunkData)) } catch (err) { console.error(`[streams] Failed to fetch chunk ${chunk.index}:`, err) } } // End the stream if recording is complete if (rec.status === "ended") { fileStream.end() } // Create StreamRecording object const recording = { title: rec.title, startedAt: rec.startedAt, endedAt: rec.endedAt || null, durationMs: rec.endedAt ? rec.endedAt - rec.startedAt : Date.now() - rec.startedAt, streamKey: rec.streamKey, isLive: rec.status === "recording", videoFile: fileStream, thumbnailData: null, metadata: rec.metadata || null, } // Push to Jazz root.streamRecordings.$jazz.push(recording) console.log(`[streams] Added recording to Jazz: ${rec.title}`) setLastSync(new Date()) } catch (err) { console.error(`[streams] Failed to create recording in Jazz:`, err) } } } catch (error) { console.error("[streams] Sync error:", error) } finally { setSyncing(false) } } const handleManualSync = () => { void syncPendingRecordings() } if (!me.$isLoaded || !root?.$isLoaded) { return (

Loading Jazz...

) } const recordings: StreamRecording[] = recordingsList?.$isLoaded ? [...recordingsList] : [] return (
{lastSync && ( Last sync: {lastSync.toLocaleTimeString()} )}
{recordings.length === 0 ? (
) : (
{recordings.map((recording, index) => ( ))}
)}
) }