Live video on the AT Protocol
79
fork

Configure Feed

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

at eli/rtmps 189 lines 5.7 kB view raw
1import { useEffect, useState } from "react"; 2import { Button, Label, Paragraph, TextArea, View, isWeb } from "tamagui"; 3import { useToastController } from "@tamagui/toast"; 4import { 5 createLivestreamRecord, 6 selectNewLivestream, 7 selectUserProfile, 8} from "features/bluesky/blueskySlice"; 9import { useAppDispatch, useAppSelector } from "store/hooks"; 10import { useLiveUser } from "hooks/useLiveUser"; 11import ThumbnailSelector from "components/thumbnail-selector"; 12import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame"; 13import { useWindowDimensions, ScrollView } from "react-native"; 14 15const Left = ({ children }: { children: React.ReactNode }) => { 16 return ( 17 <View f={2} fg={2} fb={0}> 18 {children} 19 </View> 20 ); 21}; 22 23const Right = ({ children }: { children: React.ReactNode }) => { 24 return ( 25 <View f={6} fb={0} fg={6}> 26 {children} 27 </View> 28 ); 29}; 30 31export default function CreateLivestream() { 32 const dispatch = useAppDispatch(); 33 const toast = useToastController(); 34 const userIsLive = useLiveUser(); 35 const [title, setTitle] = useState(""); 36 const [loading, setLoading] = useState(false); 37 const [customThumbnail, setCustomThumbnail] = useState<Blob | undefined>( 38 undefined, 39 ); 40 const profile = useAppSelector(selectUserProfile); 41 const newLivestream = useAppSelector(selectNewLivestream); 42 const captureFrame = useCaptureVideoFrame(); 43 const { width, height } = useWindowDimensions(); 44 45 // Responsive layout logic 46 const isWide = width > 1020; 47 const useTwoColumns = isWide; 48 49 useEffect(() => { 50 if (newLivestream?.record) { 51 toast.show("Livestream announced", { 52 message: newLivestream.record.title, 53 }); 54 setTitle(""); 55 setCustomThumbnail(undefined); 56 } 57 }, [newLivestream?.record]); 58 useEffect(() => { 59 if (newLivestream?.error) { 60 toast.show("Error creating livestream", { 61 message: newLivestream.error, 62 }); 63 } 64 }, [newLivestream?.error]); 65 const disabled = !userIsLive || loading || title === ""; 66 67 const handleSubmit = async () => { 68 setLoading(true); 69 try { 70 let thumbnailToUse = customThumbnail; 71 if (!thumbnailToUse && isWeb && captureFrame) { 72 const capturedFrame = await captureFrame(1280, 0.85); 73 if (capturedFrame) { 74 thumbnailToUse = capturedFrame; 75 } 76 } 77 78 await dispatch( 79 createLivestreamRecord({ 80 title, 81 customThumbnail: thumbnailToUse, 82 }), 83 ); 84 } catch (error) { 85 console.error("Error creating livestream:", error); 86 toast.show("Error creating livestream", { 87 message: String(error), 88 }); 89 } finally { 90 setLoading(false); 91 } 92 }; 93 94 const buttonText = loading 95 ? "Loading..." 96 : !userIsLive 97 ? "Waiting for stream to start..." 98 : "Announce Livestream!"; 99 100 return ( 101 <ScrollView 102 style={{ width: "60%" }} 103 contentContainerStyle={{ 104 flexGrow: 1, 105 justifyContent: "flex-start", 106 paddingVertical: 40, 107 }} 108 showsVerticalScrollIndicator={false} 109 > 110 <View 111 flexDirection={useTwoColumns ? "row" : "column"} 112 gap={useTwoColumns ? 48 : 16} 113 w="100%" 114 maxWidth={useTwoColumns ? 900 : undefined} 115 alignSelf="center" 116 p="$4" 117 alignItems={useTwoColumns ? "flex-start" : "stretch"} 118 justifyContent="center" 119 > 120 {/* Left column: labels and fields */} 121 <View f={2} minWidth={0} gap="$3" w={useTwoColumns ? 500 : "100%"}> 122 <Label asChild={true} display="flex"> 123 <View flexDirection="row" alignItems="center" w="100%"> 124 <Paragraph pb="$2" minWidth={100} textAlign="left"> 125 Streamer 126 </Paragraph> 127 <Paragraph pb="$2" fontWeight="bold"> 128 @{profile?.handle} 129 </Paragraph> 130 </View> 131 </Label> 132 <Label asChild={true}> 133 <View flexDirection="row" alignItems="center" w="100%"> 134 <Paragraph pb="$2" minWidth={100} textAlign="left"> 135 Title 136 </Paragraph> 137 <View flex={1}> 138 <TextArea 139 id="livestream-title" 140 value={title} 141 onChangeText={setTitle} 142 size="$4" 143 minHeight={100} 144 maxLength={140} 145 w="100%" 146 /> 147 </View> 148 </View> 149 </Label> 150 <View w="100%" alignItems="center" mt="$4"> 151 <Button 152 disabled={disabled} 153 opacity={disabled ? 0.5 : 1} 154 size="$4" 155 w="100%" 156 onPress={handleSubmit} 157 > 158 {buttonText} 159 </Button> 160 </View> 161 </View> 162 {/* Right column: thumbnail */} 163 <View 164 f={1} 165 minWidth={0} 166 gap="$4" 167 alignItems="center" 168 justifyContent="flex-start" 169 w={useTwoColumns ? 400 : "100%"} 170 style={{ 171 marginTop: 12, 172 ...(useTwoColumns ? {} : { marginLeft: 40 }), 173 }} 174 > 175 <Label asChild={true}> 176 <View flexDirection="column" alignItems="center" w="100%"> 177 <Paragraph pb={0} lineHeight={18} fontWeight="bold" mb="$2"> 178 Custom Thumbnail (Optional) 179 </Paragraph> 180 <View maxWidth={400} w="100%"> 181 <ThumbnailSelector onThumbnailSelected={setCustomThumbnail} /> 182 </View> 183 </View> 184 </Label> 185 </View> 186 </View> 187 </ScrollView> 188 ); 189}