Live video on the AT Protocol
79
fork

Configure Feed

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

components: migrate livestream

Eli Mallon 8e44a1a6 0ba67c78

+69 -106
+2 -2
js/app/components/chat/chat-box.tsx
··· 2 2 import { 3 3 useChat, 4 4 useCreateChatMessage, 5 + useLivestream, 5 6 useReplyToMessage, 6 7 useSetReplyToMessage, 7 8 } from "@streamplace/components"; ··· 22 23 import { 23 24 LivestreamViewHydrated, 24 25 usePlayerId, 25 - usePlayerLivestream, 26 26 } from "features/player/playerSlice"; 27 27 import { 28 28 chatWarn, ··· 77 77 const chatProfile = useAppSelector(selectChatProfile); 78 78 const chatWarned = useAppSelector(selectChatWarned); 79 79 const loggedOut = isReady && !userProfile; 80 - const livestream = useAppSelector(usePlayerLivestream()); 80 + const livestream = useLivestream(); 81 81 const chat = useChat(); 82 82 const textAreaRef = useRef<Input>(null); 83 83 const dispatch = useAppDispatch();
+8 -9
js/app/components/chat/chat.tsx
··· 1 - import { useChat, useSetReplyToMessage } from "@streamplace/components"; 1 + import { 2 + useChat, 3 + useProfile, 4 + useSetReplyToMessage, 5 + } from "@streamplace/components"; 2 6 import { Reply, Settings, X } from "@tamagui/lucide-icons"; 3 7 import { 4 8 createBlockRecord, 5 9 selectUserProfile, 6 10 } from "features/bluesky/blueskySlice"; 7 - import { 8 - MessageViewHydrated, 9 - usePlayerLivestream, 10 - } from "features/player/playerSlice"; 11 + import { MessageViewHydrated } from "features/player/playerSlice"; 11 12 import usePlatform from "hooks/usePlatform"; 12 13 import { useEffect, useRef, useState } from "react"; 13 14 import { Linking, TouchableOpacity } from "react-native"; ··· 37 38 const [isAtBottom, setIsAtBottom] = useState(true); 38 39 const chat = useChat(); 39 40 const scrollRef = useRef<ScrollView>(null); 40 - const livestream = useAppSelector(usePlayerLivestream()); 41 + const streamerProfile = useProfile(); 41 42 const userProfile = useAppSelector(selectUserProfile); 42 43 const myStream = !!( 43 44 userProfile && 44 - livestream && 45 45 userProfile.did && 46 - livestream.author.did && 47 - userProfile.did === livestream.author.did 46 + userProfile.did === streamerProfile?.did 48 47 ); 49 48 50 49 const handleScroll = (event: any) => {
+3 -1
js/app/components/edit-livestream.tsx
··· 1 + import { useLivestream } from "@streamplace/components"; 1 2 import { useToastController } from "@tamagui/toast"; 2 3 import { 3 4 selectNewLivestream, ··· 21 22 const [title, setTitle] = useState(""); 22 23 const [loading, setLoading] = useState(false); 23 24 const profile = useAppSelector(selectUserProfile); 25 + const livestream = useLivestream(); 24 26 const newLivestream = useAppSelector(selectNewLivestream); 25 27 26 28 useEffect(() => { ··· 57 59 await dispatch( 58 60 updateLivestreamRecord({ 59 61 title, 60 - playerId, 62 + livestream, 61 63 }), 62 64 ); 63 65 } catch (error) {
+9 -7
js/app/components/livestream/livestream.tsx
··· 1 - import { useViewers } from "@streamplace/components"; 1 + import { useLivestream, useProfile, useViewers } from "@streamplace/components"; 2 2 import { MessageCircleMore, MessageCircleOff } from "@tamagui/lucide-icons"; 3 3 import { useToastController } from "@tamagui/toast"; 4 4 import Chat from "components/chat/chat"; ··· 70 70 const [currentUserDID, setCurrentUserDID] = useState<string | null>(null); 71 71 const { fullscreen, setFullscreen } = useFullscreen(); 72 72 73 - const streamerDID = player.livestream?.author?.did; 74 - const streamerProfile = streamerDID ? profiles[streamerDID] : undefined; 73 + const livestream = useLivestream(); 74 + const streamerProfile = useProfile(); 75 + 76 + const streamerDID = livestream?.author?.did; 75 77 const streamerHandle = streamerProfile?.handle; 76 - const startTime = player.livestream?.record?.createdAt 77 - ? new Date(player.livestream?.record?.createdAt) 78 + const startTime = livestream?.record?.createdAt 79 + ? new Date(livestream?.record?.createdAt) 78 80 : undefined; 79 81 80 82 // this would all be really easy if i had library that would give me the ··· 309 311 : undefined 310 312 } 311 313 > 312 - {player.livestream?.record.title} 314 + {livestream?.record.title} 313 315 </H2> 314 316 </View> 315 317 </View> ··· 407 409 marginTop={-15} 408 410 > 409 411 <Text fontSize={18} numberOfLines={1} ellipsizeMode="tail"> 410 - {player.livestream?.record.title} 412 + {livestream?.record.title} 411 413 </Text> 412 414 </View> 413 415 </View>
+2 -6
js/app/components/player/provider.tsx
··· 70 70 props: Partial<PlayerProps> & { children: React.ReactNode }, 71 71 ) { 72 72 const dispatch = useAppDispatch(); 73 - const { pollLivestream, pollSegment, handleWebSocketMessages } = 74 - usePlayerActions(); 73 + const { pollSegment, handleWebSocketMessages } = usePlayerActions(); 75 74 76 75 const url = useAppSelector(selectUrl); 77 76 let wsUrl = url.replace(/^http\:/, "ws:"); ··· 134 133 if (!props.src) { 135 134 return; 136 135 } 137 - await Promise.all([ 138 - dispatch(pollLivestream(props.src)), 139 - dispatch(pollSegment(props.src)), 140 - ]); 136 + await Promise.all([dispatch(pollSegment(props.src))]); 141 137 }; 142 138 handle = setInterval(poll, POLL_INTERVAL); 143 139 return () => clearInterval(handle);
+6 -2
js/app/features/bluesky/blueskySlice.tsx
··· 18 18 } from "features/streamplace/streamplaceSlice"; 19 19 import Storage from "storage"; 20 20 import { 21 + LivestreamViewHydrated, 21 22 PlaceStreamChatProfile, 22 23 PlaceStreamKey, 23 24 PlaceStreamLivestream, ··· 826 827 827 828 updateLivestreamRecord: create.asyncThunk( 828 829 async ( 829 - { title, playerId }: { title: string; playerId: string }, 830 + { 831 + title, 832 + livestream, 833 + }: { title: string; livestream: LivestreamViewHydrated | null }, 830 834 thunkAPI, 831 835 ) => { 832 836 const now = new Date(); ··· 848 852 throw new Error("No profile"); 849 853 } 850 854 851 - let oldRecord = player[playerId].livestream; 855 + let oldRecord = livestream; 852 856 if (!oldRecord) { 853 857 throw new Error("No latest record"); 854 858 }
+2 -56
js/app/features/player/playerSlice.tsx
··· 57 57 ingestStarted: number | null; 58 58 ingestStarting: boolean; 59 59 ingestConnectionState: RTCPeerConnectionState | null; 60 - livestream: LivestreamViewHydrated | null; 61 60 segment: PlaceStreamSegment.Record | null; 62 61 renditions: PlaceStreamDefs.Rendition[]; 63 62 selectedRendition: string | null; ··· 115 114 ingestStarting: false, 116 115 ingestConnectionState: null, 117 116 protocol: action.payload.forceProtocol ?? PROTOCOL_WEBRTC, 118 - livestream: null, 119 117 segment: null, 120 118 renditions: [], 121 119 selectedRendition: "source", ··· 174 172 }, 175 173 ) => { 176 174 for (const message of action.payload.messages) { 177 - if (PlaceStreamLivestream.isLivestreamView(message)) { 178 - state = { 179 - ...state, 180 - [action.payload.playerId]: { 181 - ...state[action.payload.playerId], 182 - livestream: message as LivestreamViewHydrated, 183 - }, 184 - }; 185 - } else if (PlaceStreamSegment.isRecord(message)) { 175 + if (PlaceStreamSegment.isRecord(message)) { 186 176 state = { 187 177 ...state, 188 178 [action.payload.playerId]: { ··· 204 194 }, 205 195 ), 206 196 207 - pollLivestream: create.asyncThunk( 208 - async ( 209 - { playerId, user }: { playerId: string; user: string }, 210 - { getState }, 211 - ) => { 212 - const { streamplace } = getState() as { 213 - streamplace: StreamplaceState; 214 - }; 215 - const res = await fetch(`${streamplace.url}/api/livestream/${user}`); 216 - const data = (await res.json()) as LivestreamViewHydrated; 217 - return { playerId, livestream: data }; 218 - }, 219 - { 220 - pending: (state) => { 221 - // state.status = "loading"; 222 - }, 223 - fulfilled: (state, result) => { 224 - return { 225 - ...state, 226 - [result.payload.playerId]: { 227 - ...state[result.payload.playerId], 228 - livestream: result.payload.livestream, 229 - }, 230 - }; 231 - }, 232 - rejected: (state, error) => { 233 - console.error("pollLivestream rejected", error); 234 - return state; 235 - }, 236 - }, 237 - ), 238 - 239 197 pollSegment: create.asyncThunk( 240 198 async ( 241 199 { playerId, user }: { playerId: string; user: string }, ··· 320 278 selectPlayer: (state, playerId: string) => { 321 279 return state[playerId]; 322 280 }, 323 - selectLivestream: (state, playerId: string) => { 324 - return state[playerId].livestream; 325 - }, 326 281 selectSegment: (state, playerId: string) => { 327 282 return state[playerId].segment; 328 283 }, ··· 351 306 ingestConnectionState, 352 307 }); 353 308 }, 354 - pollLivestream: (user: string) => 355 - playerSlice.actions.pollLivestream({ playerId, user }), 356 309 pollSegment: (user: string) => 357 310 playerSlice.actions.pollSegment({ playerId, user }), 358 311 handleWebSocketMessages: (messages: any[]) => ··· 365 318 }; 366 319 367 320 // Action creators are generated for each case reducer function. 368 - export const { selectPlayer, selectLivestream, selectSegment } = 369 - playerSlice.selectors; 321 + export const { selectPlayer, selectSegment } = playerSlice.selectors; 370 322 export const usePlayer = (): ((state: { 371 323 player: PlayersState; 372 324 }) => PlayerState) => { 373 325 const playerId = usePlayerId(); 374 326 return (state) => state.player[playerId]; 375 - }; 376 - export const usePlayerLivestream = (): ((state: { 377 - player: PlayersState; 378 - }) => LivestreamViewHydrated | null) => { 379 - const playerId = usePlayerId(); 380 - return (state) => state.player[playerId].livestream; 381 327 }; 382 328 export const usePlayerSegment = (): ((state: { 383 329 player: PlayersState;
+27 -22
js/app/src/screens/live-dashboard.tsx
··· 1 + import { LivestreamProvider } from "@streamplace/components"; 1 2 import { Camera, FerrisWheel, X } from "@tamagui/lucide-icons"; 2 3 import { Redirect } from "components/aqlink"; 3 4 import CreateLivestream from "components/create-livestream"; ··· 96 97 ); 97 98 } 98 99 return ( 99 - <VideoElementProvider videoElement={videoElement}> 100 - <View f={1} ai="stretch" jc="center"> 101 - <View f={1} fb={0}> 102 - {topPane} 103 - {closeButton} 104 - </View> 105 - <View f={1} ai="center" jc="center" fb={0}> 106 - <ButtonSelector 107 - values={[ 108 - { label: "Create", value: "create" }, 109 - { label: "Update", value: "update" }, 110 - ]} 111 - disabledValues={playerId ? [] : ["update"]} 112 - selectedValue={page} 113 - setSelectedValue={setPage} 114 - maxWidth={250} 115 - width="100%" 116 - /> 117 - {page === "update" ? <UpdateLivestream playerId={playerId} /> : null} 118 - {page === "create" ? <CreateLivestream /> : null} 100 + <LivestreamProvider src={userProfile.did}> 101 + <VideoElementProvider videoElement={videoElement}> 102 + <View f={1} ai="stretch" jc="center"> 103 + <View f={1} fb={0}> 104 + {topPane} 105 + {closeButton} 106 + </View> 107 + <View f={1} ai="center" jc="center" fb={0}> 108 + <ButtonSelector 109 + values={[ 110 + { label: "Create", value: "create" }, 111 + { label: "Update", value: "update" }, 112 + ]} 113 + disabledValues={playerId ? [] : ["update"]} 114 + selectedValue={page} 115 + setSelectedValue={setPage} 116 + maxWidth={250} 117 + width="100%" 118 + /> 119 + {page === "update" ? ( 120 + <UpdateLivestream playerId={playerId} /> 121 + ) : null} 122 + {page === "create" ? <CreateLivestream /> : null} 123 + </View> 119 124 </View> 120 - </View> 121 - </VideoElementProvider> 125 + </VideoElementProvider> 126 + </LivestreamProvider> 122 127 ); 123 128 } 124 129
+8 -1
js/components/src/livestream-provider/index.tsx
··· 1 - import React, { useRef } from "react"; 1 + import React, { useContext, useRef } from "react"; 2 2 import { LivestreamContext, makeLivestreamStore } from "../livestream-store"; 3 3 import { useLivestreamWebsocket } from "./websocket"; 4 4 ··· 9 9 children: React.ReactNode; 10 10 src: string; 11 11 }) { 12 + const context = useContext(LivestreamContext); 12 13 const store = useRef(makeLivestreamStore()).current; 14 + if (context) { 15 + // this is ok, there's use cases for having one in another 16 + // like having a player component that's independently usable 17 + // but can also be embedded within an entire livestream page 18 + return <>{children}</>; 19 + } 13 20 (window as any).livestreamStore = store; 14 21 return ( 15 22 <LivestreamContext.Provider value={{ store: store }}>
+2
js/components/src/livestream-store/livestream-store.tsx
··· 46 46 export const useProfile = () => useLivestreamStore((x) => x.profile); 47 47 48 48 export const useViewers = () => useLivestreamStore((x) => x.viewers); 49 + 50 + export const useLivestream = () => useLivestreamStore((x) => x.livestream);