Live video on the AT Protocol

bento updates + more accurate data

+32 -204
-2
js/app/components/live-dashboard/bento-grid.tsx
··· 4 4 import Header from "./header"; 5 5 import LivestreamPanel from "./livestream-panel"; 6 6 import ModActions from "./mod-actions"; 7 - import QuickStats from "./quick-stats"; 8 7 import StreamMonitor from "./stream-monitor"; 9 8 10 9 const { flex, p, gap, layout, bg } = zero; ··· 53 52 <View style={[layout.flex.row, gap.all[4], flex.values[1]]}> 54 53 <ModActions isLive={isLive} /> 55 54 </View> 56 - <QuickStats /> 57 55 </View> 58 56 59 57 <View
+11 -40
js/app/components/live-dashboard/header.tsx
··· 2 2 useLivestreamStore, 3 3 usePlayerStore, 4 4 useProfile, 5 + useSegment, 5 6 zero, 6 7 } from "@streamplace/components"; 7 - import { 8 - Activity, 9 - Monitor, 10 - Radio, 11 - Signal, 12 - Users, 13 - Wifi, 14 - } from "@tamagui/lucide-icons"; 8 + import { Activity, Car, Radio, Signal, Users } from "@tamagui/lucide-icons"; 15 9 import { Text, View } from "react-native"; 16 10 import { useLiveUser } from "../../hooks/useLiveUser"; 17 11 import { useSegmentTiming } from "../../hooks/useSegmentTiming"; ··· 119 113 const isUserLive = useLiveUser(); 120 114 const viewers = useLivestreamStore((x) => x.viewers); 121 115 const segmentTiming = useSegmentTiming(); 116 + const seg = useSegment(); 122 117 const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState); 123 118 const ingestStarted = usePlayerStore((x) => x.ingestStarted); 124 119 ··· 151 146 } 152 147 }; 153 148 154 - const getFpsStatus = (fps: number): "good" | "warning" | "error" => { 155 - if (fps >= 30) return "good"; 156 - if (fps >= 20) return "warning"; 157 - return "error"; 158 - }; 159 - 160 - const getBitrateStatus = (bitrate: string): "good" | "warning" | "error" => { 161 - const value = parseInt(bitrate); 162 - if (value >= 2000) return "good"; 163 - if (value >= 1000) return "warning"; 164 - return "error"; 165 - }; 149 + console.log(seg); 166 150 167 151 return ( 168 152 <View ··· 198 182 /> 199 183 <MetricItem 200 184 icon={Activity} 201 - label="Segments" 185 + label="Time between Segments" 202 186 value={`${segmentTiming.timeBetweenSegments || 0}ms`} 203 187 status={ 204 188 segmentTiming.connectionQuality === "good" ··· 209 193 } 210 194 /> 211 195 <MetricItem 212 - icon={Monitor} 213 - label="Quality" 214 - value={segmentTiming.connectionQuality.toUpperCase()} 215 - status={ 216 - segmentTiming.connectionQuality === "good" 217 - ? "good" 218 - : segmentTiming.connectionQuality === "degraded" 219 - ? "warning" 220 - : "error" 196 + icon={Car} 197 + label="Bitrate" 198 + value={ 199 + seg?.size 200 + ? `${((seg.size * 8) / ((seg.duration || 1000000000) / 1000000000) / 1000 / 1000).toFixed(2)} kbps` 201 + : "0 kbps" 221 202 } 222 - /> 223 - <MetricItem 224 - icon={Radio} 225 - label="Connection" 226 - value={ingestConnectionState || "disconnected"} 227 - /> 228 - <MetricItem 229 - icon={Wifi} 230 - label="Range" 231 - value={segmentTiming.range ? `${segmentTiming.range}ms` : "N/A"} 232 203 /> 233 204 <MetricItem icon={Signal} label="Uptime" value={getUptime()} /> 234 205 </>
-85
js/app/components/live-dashboard/quick-stats.tsx
··· 1 - import { useLivestreamStore, zero } from "@streamplace/components"; 2 - import { Heart, MessageCircle, Users } from "@tamagui/lucide-icons"; 3 - import { Text, View } from "react-native"; 4 - import { useSegmentTiming } from "../../hooks/useSegmentTiming"; 5 - 6 - const { flex, bg, r, p, text, layout, gap, borderRadius } = zero; 7 - 8 - interface StatCardProps { 9 - icon: any; 10 - label: string; 11 - value: string; 12 - color: "blue" | "red" | "green"; 13 - } 14 - 15 - function StatCard({ icon: Icon, label, value, color }: StatCardProps) { 16 - const colors = { 17 - blue: { bg: bg.blue[500], text: text.blue[100] }, 18 - red: { bg: bg.red[500], text: text.red[100] }, 19 - green: { bg: bg.green[500], text: text.green[100] }, 20 - }; 21 - 22 - return ( 23 - <View 24 - style={[ 25 - flex.values[1], 26 - colors[color].bg, 27 - r[3], 28 - p[4], 29 - layout.flex.row, 30 - layout.flex.alignCenter, 31 - gap.all[3], 32 - borderRadius["2xl"], 33 - ]} 34 - > 35 - <Icon size={24} color="white" /> 36 - <View> 37 - <Text style={[text.white, { fontSize: 24, fontWeight: "700" }]}> 38 - {value} 39 - </Text> 40 - <Text style={[colors[color].text, { fontSize: 12, fontWeight: "500" }]}> 41 - {label} 42 - </Text> 43 - </View> 44 - </View> 45 - ); 46 - } 47 - 48 - export default function QuickStats() { 49 - // Get real data from stores 50 - const viewers = useLivestreamStore((x) => x.viewers); 51 - const chat = useLivestreamStore((x) => x.chat); 52 - const segmentTiming = useSegmentTiming(); 53 - 54 - // Calculate stats from real data 55 - const viewerCount = viewers || 0; 56 - const messageCount = chat?.length || 0; 57 - 58 - // Count likes/hearts in chat messages (simplified - could be more sophisticated) 59 - const likeCount = 0; 60 - 61 - return ( 62 - <View 63 - style={[flex.values[1], gap.all[4], layout.flex.row, { maxHeight: 64 }]} 64 - > 65 - <StatCard 66 - icon={Users} 67 - label="Viewers" 68 - value={viewerCount.toLocaleString()} 69 - color="blue" 70 - /> 71 - <StatCard 72 - icon={Heart} 73 - label="Likes" 74 - value={likeCount.toString()} 75 - color="red" 76 - /> 77 - <StatCard 78 - icon={MessageCircle} 79 - label="Messages" 80 - value={messageCount.toString()} 81 - color="green" 82 - /> 83 - </View> 84 - ); 85 - }
-57
js/app/components/live-dashboard/stream-controls.tsx
··· 1 - import { zero } from "@streamplace/components"; 2 - import { Play, Square } from "@tamagui/lucide-icons"; 3 - import { Pressable, Text, View } from "react-native"; 4 - 5 - const { flex, bg, r, borders, p, px, py, text, layout, gap } = zero; 6 - 7 - // Mock data - replace with real data from your stores 8 - const mockStats = { 9 - uptime: "02:34:12", 10 - }; 11 - 12 - interface StreamControlsProps { 13 - isLive: boolean; 14 - } 15 - 16 - export default function StreamControls({ isLive }: StreamControlsProps) { 17 - return ( 18 - <View 19 - style={[ 20 - flex.values[1], 21 - bg.gray[800], 22 - r[3], 23 - borders.width.thin, 24 - borders.color.gray[700], 25 - p[4], 26 - layout.flex.row, 27 - layout.flex.alignCenter, 28 - gap.all[3], 29 - ]} 30 - > 31 - <Pressable 32 - style={[ 33 - bg[isLive ? "red" : "green"][500], 34 - r[2], 35 - px[4], 36 - py[3], 37 - layout.flex.row, 38 - layout.flex.alignCenter, 39 - gap.all[2], 40 - ]} 41 - > 42 - {isLive ? ( 43 - <Square size={20} color="white" /> 44 - ) : ( 45 - <Play size={20} color="white" /> 46 - )} 47 - <Text style={[text.white, { fontSize: 16, fontWeight: "600" }]}> 48 - {isLive ? "Stop Stream" : "Go Live"} 49 - </Text> 50 - </Pressable> 51 - 52 - <Text style={[text.gray[400], { fontSize: 14 }]}> 53 - Uptime: {mockStats.uptime} 54 - </Text> 55 - </View> 56 - ); 57 - }
+21 -20
js/app/components/live-dashboard/stream-monitor.tsx
··· 1 1 import { 2 2 Player, 3 + useLivestream, 3 4 useLivestreamStore, 4 5 usePlayerStore, 5 6 zero, ··· 26 27 const isUserLive = useLiveUser(); 27 28 const profile = useLivestreamStore((x) => x.profile); 28 29 const ingestConnectionState = usePlayerStore((x) => x.ingestConnectionState); 30 + const ls = useLivestream(); 29 31 const segmentTiming = useSegmentTiming(); 30 32 31 33 // Use hook data primarily, fallback to props ··· 73 75 layout.flex.column, 74 76 ]} 75 77 > 78 + <View style={[flex.values[1], layout.flex.center, bg.gray[900]]}> 79 + {isLive && userProfile ? ( 80 + <Player src={userProfile.did} name={userProfile.handle} /> 81 + ) : ( 82 + <View style={[layout.flex.center, { gap: 12 }]}> 83 + <Camera size={48} color="#6b7280" /> 84 + <Text style={[text.gray[400]]}> 85 + {!userProfile ? "No Profile" : "Stream Offline"} 86 + </Text> 87 + {ingestConnectionState && ( 88 + <Text style={[text.gray[500], { fontSize: 12 }]}> 89 + Connection: {ingestConnectionState} 90 + </Text> 91 + )} 92 + </View> 93 + )} 94 + </View> 76 95 <View 77 96 style={[ 78 97 layout.flex.row, ··· 84 103 ]} 85 104 > 86 105 <Text style={[text.white, { fontSize: 18, fontWeight: "600" }]}> 87 - Stream Monitor 106 + {ls?.record.title || "Stream Title"} 88 107 </Text> 89 - <View style={[layout.flex.row, layout.flex.alignCenter, { gap: 8 }]}> 108 + <View style={[layout.flex.row, layout.flex.center, { gap: 8 }]}> 90 109 {getConnectionIcon()} 91 110 <View style={[w[2], h[2], r[1], bg[getConnectionColor()][500]]} /> 92 111 <Text style={[text.gray[400], { fontSize: 14 }]}> ··· 98 117 </Text> 99 118 )} 100 119 </View> 101 - </View> 102 - 103 - <View style={[flex.values[1], layout.flex.center, bg.gray[900]]}> 104 - {isLive && userProfile ? ( 105 - <Player src={userProfile.did} name={userProfile.handle} /> 106 - ) : ( 107 - <View style={[layout.flex.center, { gap: 12 }]}> 108 - <Camera size={48} color="#6b7280" /> 109 - <Text style={[text.gray[400]]}> 110 - {!userProfile ? "No Profile" : "Stream Offline"} 111 - </Text> 112 - {ingestConnectionState && ( 113 - <Text style={[text.gray[500], { fontSize: 12 }]}> 114 - Connection: {ingestConnectionState} 115 - </Text> 116 - )} 117 - </View> 118 - )} 119 120 </View> 120 121 </View> 121 122 );