Live video on the AT Protocol
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}