Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at natb/reporting-interface 254 lines 7.3 kB view raw
1import { LivestreamProvider } from "@streamplace/components"; 2import { Camera, FerrisWheel, X } from "@tamagui/lucide-icons"; 3import { Redirect } from "components/aqlink"; 4import CreateLivestream from "components/create-livestream"; 5import UpdateLivestream from "components/edit-livestream"; 6import StreamKeyScreen from "components/live-dashboard/stream-key"; 7import Waiting from "components/live-dashboard/waiting"; 8import Loading from "components/loading/loading"; 9import { Player } from "components/player/player"; 10import Popup from "components/popup"; 11import ButtonSelector from "components/ui/button-selector"; 12import { VideoElementProvider } from "contexts/VideoElementContext"; 13import { 14 createServerSettingsRecord, 15 getServerSettingsFromPDS, 16 selectIsReady, 17 selectServerSettings, 18 selectUserProfile, 19} from "features/bluesky/blueskySlice"; 20import { useLiveUser } from "hooks/useLiveUser"; 21import React, { useCallback, useEffect, useState } from "react"; 22import { useAppDispatch, useAppSelector } from "store/hooks"; 23import { Button, H3, H6, isWeb, Text, View } from "tamagui"; 24 25enum StreamSource { 26 Start, 27 Camera, 28 StreamKey, 29} 30 31export default function LiveDashboard() { 32 const isReady = useAppSelector(selectIsReady); 33 const userProfile = useAppSelector(selectUserProfile); 34 const [streamSource, setStreamSource] = useState(StreamSource.Start); 35 const serverSettings = useAppSelector(selectServerSettings); 36 const isLive = useLiveUser(); 37 const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>( 38 null, 39 ); 40 const [gotSettings, setGotSettings] = useState(false); 41 const dispatch = useAppDispatch(); 42 useEffect(() => { 43 if (isReady) { 44 (async () => { 45 await dispatch(getServerSettingsFromPDS()); 46 setGotSettings(true); 47 })(); 48 } 49 }, [isReady]); 50 51 let madeChoiceAboutDebugRecording = true; 52 if (gotSettings && serverSettings?.debugRecording === undefined) { 53 madeChoiceAboutDebugRecording = false; 54 } 55 56 const [page, setPage] = useState<"update" | "create">("create"); 57 58 const videoRef = useCallback((node: HTMLVideoElement | null) => { 59 if (node !== null) { 60 setVideoElement(node); 61 } 62 }, []); 63 if (!isReady) { 64 return <Loading />; 65 } 66 if (!userProfile) { 67 return <Redirect to={{ screen: "Login" }} />; 68 } 69 let topPane: React.ReactNode; 70 let params = new URLSearchParams(); 71 if (isWeb) { 72 params = new URLSearchParams(window.location.search); 73 } 74 75 if (isLive && streamSource !== StreamSource.Camera) { 76 topPane = ( 77 <Player 78 src={userProfile.did} 79 name={userProfile.handle} 80 videoRef={videoRef} 81 /> 82 ); 83 } else if (streamSource === StreamSource.Start) { 84 topPane = <StreamSourcePicker onPick={setStreamSource} />; 85 } else if (streamSource === StreamSource.Camera) { 86 topPane = ( 87 <Player src={userProfile.did} name={userProfile.handle} ingest={true} /> 88 ); 89 } else if (streamSource === StreamSource.StreamKey) { 90 topPane = <StreamKeyScreen />; 91 } else { 92 throw new Error("Invalid stream source"); 93 } 94 let closeButton: React.ReactNode = <></>; 95 if (streamSource !== StreamSource.Start && !isLive) { 96 closeButton = ( 97 <Button 98 position="absolute" 99 top="$0" 100 right="$0" 101 onPress={(e) => { 102 e.stopPropagation(); 103 setStreamSource(StreamSource.Start); 104 }} 105 zIndex={1000} 106 marginTop={10} 107 marginRight={10} 108 > 109 <X /> 110 </Button> 111 ); 112 } 113 114 return ( 115 <LivestreamProvider src={userProfile.did}> 116 <VideoElementProvider videoElement={videoElement}> 117 <View f={1} ai="stretch" jc="center"> 118 <View f={1} fb={0}> 119 {topPane} 120 {closeButton} 121 </View> 122 <View f={1} ai="center" jc="center" fb={0}> 123 <ButtonSelector 124 values={[ 125 { label: "Create", value: "create" }, 126 { label: "Update", value: "update" }, 127 ]} 128 disabledValues={isLive ? [] : ["update"]} 129 selectedValue={page} 130 setSelectedValue={setPage} 131 maxWidth={250} 132 width="100%" 133 /> 134 {page === "update" && isLive ? <UpdateLivestream /> : null} 135 {page === "create" ? <CreateLivestream /> : null} 136 </View> 137 {madeChoiceAboutDebugRecording ? null : <DebugRecordingPopup />} 138 </View> 139 </VideoElementProvider> 140 </LivestreamProvider> 141 ); 142} 143 144const elems = [ 145 { 146 title: "Stream your camera!", 147 Icon: Camera, 148 to: StreamSource.Camera, 149 }, 150 { 151 title: "Stream from OBS!", 152 Icon: FerrisWheel, 153 to: StreamSource.StreamKey, 154 }, 155]; 156 157export function DebugRecordingPopup() { 158 const dispatch = useAppDispatch(); 159 const serverSettings = useAppSelector(selectServerSettings) || {}; 160 const opt = (choice) => () => 161 dispatch( 162 createServerSettingsRecord({ 163 ...serverSettings, 164 debugRecording: choice, 165 }), 166 ); 167 return ( 168 <Popup 169 onClose={opt(false)} 170 containerProps={{ 171 bottom: "$8", 172 zIndex: 1000, 173 }} 174 bubbleProps={{ 175 backgroundColor: "$accentBackground", 176 gap: "$3", 177 maxWidth: 400, 178 }} 179 > 180 <H3 textAlign="center">Debug Recording</H3> 181 <Text> 182 Streamplace is beta software and it helps us to archive livestreams so 183 we can later use them for debugging. Would you like to opt in to debug 184 recording? 185 </Text> 186 <View flexDirection="row" gap="$2" f={1}> 187 <Button f={3} backgroundColor="$accentColor" onPress={opt(true)}> 188 Allow 189 </Button> 190 <Button f={3} onPress={opt(false)}> 191 Don't Allow 192 </Button> 193 </View> 194 </Popup> 195 ); 196} 197 198export function StreamSourcePicker({ 199 onPick, 200}: { 201 onPick: (source: StreamSource) => void; 202}) { 203 const isReady = useAppSelector(selectIsReady); 204 const userProfile = useAppSelector(selectUserProfile); 205 if (!isReady) { 206 return <Loading />; 207 } 208 if (!userProfile) { 209 return <Redirect to={{ screen: "Login" }} />; 210 } 211 return ( 212 <View 213 f={1} 214 jc="space-around" 215 ai="stretch" 216 padding="$3" 217 flexDirection="row" 218 backgroundColor="$gray1" 219 > 220 <View f={1} maxWidth={250} alignItems="stretch" justifyContent="center"> 221 {elems.map(({ Icon, title, to }, i) => ( 222 <React.Fragment key={i}> 223 <View 224 f={1} 225 flexDirection="row" 226 ai="center" 227 jc="space-between" 228 backgroundColor="$accentColor" 229 // padding="$5" 230 borderRadius="$10" 231 cursor="pointer" 232 onPress={() => onPick(to)} 233 flexGrow={0} 234 flexBasis={75} 235 > 236 <View padding="$5" paddingRight={0}> 237 <Icon size={48} /> 238 </View> 239 <Text f={1} textAlign="right" paddingRight="$5"> 240 {title} 241 </Text> 242 </View> 243 {i < elems.length - 1 && ( 244 <View jc="center" ai="center"> 245 <H6 padding="$5">OR</H6> 246 </View> 247 )} 248 </React.Fragment> 249 ))} 250 <Waiting /> 251 </View> 252 </View> 253 ); 254}