Live video on the AT Protocol
79
fork

Configure Feed

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

at natb/changesets 241 lines 7.1 kB view raw
1import { useNavigation } from "@react-navigation/native"; 2import { 3 Button, 4 layout, 5 LivestreamProvider, 6 Player as PlayerInnerInner, 7 PlayerProps, 8 PlayerProvider, 9 Text, 10 usePlayerDimensions, 11 usePlayerStore, 12 View, 13} from "@streamplace/components"; 14import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; 15import { ArrowLeft, ArrowRight } from "@tamagui/lucide-icons"; 16import { selectUserProfile } from "features/bluesky/blueskySlice"; 17import { useLiveUser } from "hooks/useLiveUser"; 18import { useSidebarControl } from "hooks/useSidebarControl"; 19import { useEffect, useState } from "react"; 20import { Animated, ScrollView } from "react-native"; 21import { useAppSelector } from "store/hooks"; 22import { BottomMetadata } from "./bottom-metadata"; 23import { DesktopChatPanel } from "./chat"; 24import { DesktopUi } from "./desktop-ui"; 25import { OfflineCounter } from "./offline-counter"; 26import { MobileUi } from "./ui"; 27import { useResponsiveLayout } from "./useResponsiveLayout"; 28 29export function Player( 30 props: Partial<PlayerProps> & { 31 setFullscreen?: (fullscreen: boolean) => void; 32 }, 33) { 34 const [showChat, setShowChat] = useState(true); 35 const { shouldShowChatSidePanel, chatPanelWidth, safeAreaInsets } = 36 useResponsiveLayout(); 37 const chatVisible = shouldShowChatSidePanel && showChat; 38 39 const [isStreamingElsewhere, setIsStreamingElsewhere] = useState< 40 boolean | null 41 >(null); 42 // are we currently streaming on another device? 43 const userIsLive = useLiveUser(); 44 const userProfile = useAppSelector(selectUserProfile); 45 46 useEffect(() => { 47 if (props.ingest && userIsLive && isStreamingElsewhere === null) { 48 setIsStreamingElsewhere(true); 49 } else if (props.ingest && userIsLive === false) { 50 setIsStreamingElsewhere(false); 51 } 52 }, [userIsLive]); 53 54 const navigation = useNavigation(); 55 56 if (isStreamingElsewhere) { 57 return ( 58 <View style={[layout.flex.center, h.percent[100], gap.all[4]]}> 59 <Text weight="semibold" size="3xl" style={[pt[2]]}> 60 Oeps! 61 </Text> 62 <View> 63 <Text center>You're already streaming from another device.</Text> 64 <Text>Please end your other stream before starting one here.</Text> 65 </View> 66 <View 67 style={[ 68 layout.flex.row, 69 w.percent[100], 70 gap.column[2], 71 layout.flex.center, 72 ]} 73 > 74 <Button 75 variant="secondary" 76 style={[w.percent[40]]} 77 onPress={() => 78 navigation.canGoBack() 79 ? navigation.goBack() 80 : navigation.navigate("Home", { screen: "StreamList" }) 81 } 82 > 83 <View 84 centered 85 style={[layout.flex.center, layout.flex.row, gap.all[1]]} 86 > 87 <ArrowLeft /> 88 <Text>Back</Text> 89 </View> 90 </Button> 91 {userProfile?.did && ( 92 <Button 93 style={[w.percent[40]]} 94 onPress={() => 95 navigation.navigate("Home", { 96 screen: "Stream", 97 params: { user: userProfile?.did }, 98 }) 99 } 100 > 101 <View 102 centered 103 style={[layout.flex.center, layout.flex.row, gap.all[1]]} 104 > 105 <Text>Your stream</Text> 106 <ArrowRight /> 107 </View> 108 </Button> 109 )} 110 </View> 111 </View> 112 ); 113 } 114 115 return ( 116 <LivestreamProvider src={props.src ?? ""}> 117 <PlayerProvider defaultId={props.playerId || undefined}> 118 <View 119 style={{ 120 flexDirection: chatVisible ? "row" : "column", 121 flex: 1, 122 width: "100%", 123 height: "100%", 124 paddingLeft: safeAreaInsets.left, 125 paddingRight: safeAreaInsets.right, 126 }} 127 > 128 <PlayerInner 129 {...props} 130 showChat={showChat} 131 setShowChat={setShowChat} 132 /> 133 {shouldShowChatSidePanel ? ( 134 <DesktopChatPanel 135 chatVisible={chatVisible} 136 chatPanelWidth={chatPanelWidth} 137 safeAreaInsets={safeAreaInsets} 138 /> 139 ) : ( 140 <MobileUi /> 141 )} 142 </View> 143 </PlayerProvider> 144 </LivestreamProvider> 145 ); 146} 147 148export function PlayerInner( 149 props: Partial<PlayerProps> & { 150 showChat: boolean; 151 setShowChat: (show: boolean) => void; 152 }, 153) { 154 let sb = useSidebarControl(); 155 let fullscreen = usePlayerStore((x) => x.fullscreen); 156 const { 157 shouldShowChatSidePanel, 158 chatPanelWidth, 159 screenWidth, 160 contentWidth, 161 availableHeight, 162 } = useResponsiveLayout({ 163 sidebarWidth: sb.animatedWidth, 164 sidebarHidden: !sb.isActive, 165 showChatSidePanelOnLandscape: props.showChat, 166 }); 167 168 // content info 169 const { width, height } = usePlayerDimensions(); 170 171 // Calculate aspect ratio and determine if we're in desktop mode 172 const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; 173 const isDesktopMode = shouldShowChatSidePanel || screenWidth > 768; 174 175 // Calculate optimal height for desktop mode (90% of available height) 176 const maxDesktopHeight = availableHeight * 0.8; 177 const chatVisible = shouldShowChatSidePanel && props.showChat; 178 179 const calculatedWidth = chatVisible 180 ? contentWidth - chatPanelWidth 181 : contentWidth; 182 183 const calculatedHeight = isDesktopMode 184 ? Math.min(calculatedWidth / aspectRatio, maxDesktopHeight) 185 : height; 186 187 const showBottomMetaPanel = aspectRatio > 1 && screenWidth > 980; 188 189 // Direct responsive styling without animations 190 const playerStyle = { 191 width: calculatedWidth, 192 height: calculatedHeight, 193 }; 194 // i don't really like this, but it's the only way to ensure the 195 // player is sized correctly on both desktop and mobile views 196 const ContainerElement = showBottomMetaPanel ? ScrollView : View; 197 return ( 198 <ContainerElement 199 style={ 200 shouldShowChatSidePanel 201 ? { 202 height: "100%", 203 width: calculatedWidth, // Add explicit width 204 } 205 : { 206 height: "100%", 207 flex: 1, 208 } 209 } 210 contentContainerStyle={{ 211 width: calculatedWidth, 212 }} 213 showsVerticalScrollIndicator={false} 214 bounces={false} 215 > 216 <Animated.View 217 style={[ 218 showBottomMetaPanel 219 ? { 220 width: calculatedWidth, 221 height: calculatedHeight, 222 } 223 : { 224 flex: 1, 225 }, 226 ]} 227 > 228 <PlayerInnerInner {...props}> 229 {(showBottomMetaPanel || fullscreen) && <DesktopUi />} 230 <OfflineCounter isMobile={true} /> 231 </PlayerInnerInner> 232 </Animated.View> 233 {showBottomMetaPanel && ( 234 <BottomMetadata 235 setShowChat={props.setShowChat} 236 showChat={props.showChat} 237 /> 238 )} 239 </ContainerElement> 240 ); 241}