Live video on the AT Protocol

player: expose videoref for others to use

authored by Eli Mallon and committed by Natalie B c4e1e172 86710223

+66 -49
+2 -2
js/app/components/player/fullscreen.native.tsx
··· 150 150 ]} 151 151 > 152 152 <VideoRetry {...props}> 153 - <Video {...props} videoRef={ref} /> 153 + <Video {...props} nativeVideoRef={ref} /> 154 154 </VideoRetry> 155 155 <PlayerLoading {...props} /> 156 156 <Controls {...props} setFullscreen={setFullscreen} /> ··· 165 165 <PlayerLoading {...props}></PlayerLoading> 166 166 <Controls {...props} setFullscreen={setFullscreen} /> 167 167 <VideoRetry {...props}> 168 - <Video {...props} videoRef={ref} /> 168 + <Video {...props} nativeVideoRef={ref} /> 169 169 </VideoRetry> 170 170 </> 171 171 );
+11 -3
js/app/components/player/fullscreen.tsx
··· 1 - import { useEffect, useRef } from "react"; 1 + import { useCallback, useEffect, useRef } from "react"; 2 2 import { TamaguiElement, View } from "tamagui"; 3 3 import Controls from "./controls"; 4 4 import PlayerLoading from "./player-loading"; ··· 8 8 9 9 export default function Fullscreen(props: PlayerProps) { 10 10 const divRef = useRef<TamaguiElement>(null); 11 - const videoRef = useRef<HTMLVideoElement>(null); 11 + const videoRef = useRef<HTMLVideoElement | null>(null); 12 + const videoCallback = useCallback((node: HTMLVideoElement | null) => { 13 + videoRef.current = node; 14 + if (typeof props.videoRef === "function") { 15 + props.videoRef(node); 16 + } else if (props.videoRef) { 17 + props.videoRef.current = node; 18 + } 19 + }, []); 12 20 13 21 const setFullscreen = (on: boolean) => { 14 22 if (!divRef.current) { ··· 68 76 <PlayerLoading {...props}></PlayerLoading> 69 77 <Controls {...props} setFullscreen={setFullscreen} /> 70 78 <VideoRetry {...props}> 71 - <Video {...props} videoRef={videoRef} /> 79 + <Video {...props} videoRef={videoCallback} /> 72 80 </VideoRetry> 73 81 </View> 74 82 );
+1
js/app/components/player/player.tsx
··· 154 154 muteWasForced: muteWasForced, 155 155 setMuteWasForced: setMuteWasForced, 156 156 embedded: props.embedded ?? false, 157 + videoRef: props.videoRef, 157 158 ...props, 158 159 }; 159 160 return (
+5
js/app/components/player/props.tsx
··· 1 + import { VideoView } from "expo-video"; 1 2 import { Rendition } from "lexicons/types/place/stream/defs"; 2 3 3 4 export enum IngestMediaSource { ··· 40 41 muteWasForced: boolean; 41 42 setMuteWasForced: (muteWasForced: boolean) => void; 42 43 embedded: boolean; 44 + videoRef: 45 + | React.MutableRefObject<HTMLVideoElement | null> 46 + | ((instance: HTMLVideoElement | null) => void) 47 + | undefined; 43 48 }; 44 49 45 50 export type PlayerEvent = {
+2 -2
js/app/components/player/video.native.tsx
··· 14 14 // } 15 15 16 16 export default function NativeVideo( 17 - props: PlayerProps & { videoRef: React.RefObject<VideoView> }, 17 + props: PlayerProps & { nativeVideoRef: React.RefObject<VideoView> }, 18 18 ) { 19 19 const protocol = useAppSelector(usePlayerProtocol()); 20 20 if (protocol === PROTOCOL_WEBRTC) { ··· 77 77 return ( 78 78 <VideoView 79 79 style={{ flex: 1, backgroundColor: "#111" }} 80 - ref={props.videoRef} 80 + ref={props.nativeVideoRef} 81 81 player={player} 82 82 allowsFullscreen 83 83 nativeControls={props.fullscreen}
+36 -41
js/app/components/player/video.tsx
··· 29 29 30 30 type VideoProps = PlayerProps & { url: string }; 31 31 32 - export default function WebVideo( 33 - props: PlayerProps & { videoRef: RefObject<HTMLVideoElement> }, 34 - ) { 32 + export default function WebVideo(props: PlayerProps) { 35 33 const inProto = useAppSelector(usePlayerProtocol()); 36 34 const { url, protocol } = srcToUrl(props, inProto); 37 - useEffect(() => { 38 - if (props.playTime == 0) { 39 - return; 40 - } 41 - if (props.videoRef.current) { 42 - props.videoRef.current.play(); 43 - } 44 - }, [props.playTime]); 45 35 if (props.ingest) { 46 36 return <WebcamIngestPlayer url={url} {...props} />; 47 37 } ··· 69 59 }; 70 60 71 61 const VideoElement = forwardRef( 72 - (props: VideoProps, ref: ForwardedRef<HTMLVideoElement>) => { 62 + (props: VideoProps, ref: ForwardedRef<HTMLVideoElement | null>) => { 73 63 const event = (evType) => (e) => { 74 64 console.log(evType); 75 65 const now = new Date(); ··· 188 178 }, 189 179 ); 190 180 191 - export function ProgressiveMP4Player( 192 - props: VideoProps & { videoRef: RefObject<HTMLVideoElement> }, 193 - ) { 194 - return <VideoElement {...props} ref={props.videoRef} />; 181 + export function ProgressiveMP4Player(props: VideoProps) { 182 + return <VideoElement {...props} />; 195 183 } 196 184 197 - export function ProgressiveWebMPlayer( 198 - props: VideoProps & { videoRef: RefObject<HTMLVideoElement> }, 199 - ) { 200 - return <VideoElement {...props} ref={props.videoRef} />; 185 + export function ProgressiveWebMPlayer(props: VideoProps) { 186 + return <VideoElement {...props} />; 201 187 } 202 188 203 - export function HLSPlayer( 204 - props: VideoProps & { videoRef: RefObject<HTMLVideoElement> }, 205 - ) { 206 - const videoRef = props.videoRef; 189 + export function HLSPlayer(props: VideoProps) { 190 + const localRef = useRef<HTMLVideoElement | null>(null); 191 + const refCallback = useCallback((node: HTMLVideoElement | null) => { 192 + localRef.current = node; 193 + if (typeof props.videoRef === "function") { 194 + props.videoRef(node); 195 + } else if (props.videoRef) { 196 + ( 197 + props.videoRef as React.MutableRefObject<HTMLVideoElement | null> 198 + ).current = node; 199 + } 200 + }, []); 207 201 useEffect(() => { 208 - if (!videoRef.current) { 202 + if (!localRef.current) { 209 203 return; 210 204 } 211 205 if (Hls.isSupported()) { ··· 213 207 var hls = new Hls({ maxAudioFramesDrift: 20 }); 214 208 hls.loadSource(props.url); 215 209 try { 216 - hls.attachMedia(videoRef.current); 210 + hls.attachMedia(localRef.current); 217 211 } catch (e) { 218 212 console.error("error on attachMedia"); 219 213 hls.stopLoad(); 220 214 return; 221 215 } 222 216 hls.on(Hls.Events.MANIFEST_PARSED, () => { 223 - if (!videoRef.current) { 217 + if (!localRef.current) { 224 218 return; 225 219 } 226 - videoRef.current.play(); 220 + localRef.current.play(); 227 221 }); 228 222 return () => { 229 223 hls.stopLoad(); 230 224 }; 231 - } else if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { 232 - videoRef.current.src = props.url; 233 - videoRef.current.addEventListener("canplay", () => { 234 - if (!videoRef.current) { 225 + } else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) { 226 + localRef.current.src = props.url; 227 + localRef.current.addEventListener("canplay", () => { 228 + if (!localRef.current) { 235 229 return; 236 230 } 237 - videoRef.current.play(); 231 + localRef.current.play(); 238 232 }); 239 233 } 240 - }, [videoRef.current]); 241 - return <VideoElement {...props} ref={videoRef} />; 234 + }, [localRef.current]); 235 + return <VideoElement {...props} ref={refCallback} />; 242 236 } 243 237 244 - export function WebRTCPlayer( 245 - props: VideoProps & { videoRef: RefObject<HTMLVideoElement> }, 246 - ) { 238 + export function WebRTCPlayer(props: VideoProps) { 247 239 const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>( 248 240 null, 249 241 ); ··· 251 243 const handleRef = useCallback((node: HTMLVideoElement | null) => { 252 244 if (node) { 253 245 setVideoElement(node); 246 + } 247 + if (typeof props.videoRef === "function") { 248 + props.videoRef(node); 249 + } else if (props.videoRef) { 250 + props.videoRef.current = node; 254 251 } 255 252 }, []); 256 253 ··· 319 316 return <VideoElement {...props} ref={handleRef} />; 320 317 } 321 318 322 - export function WebcamIngestPlayer( 323 - props: VideoProps & { videoRef: RefObject<HTMLVideoElement> }, 324 - ) { 319 + export function WebcamIngestPlayer(props: VideoProps) { 325 320 const dispatch = useAppDispatch(); 326 321 const player = useAppSelector(usePlayer()); 327 322 const storedKey = useAppSelector(selectStoredKey);
+9 -1
js/app/src/screens/live-dashboard.tsx
··· 8 8 } from "features/bluesky/blueskySlice"; 9 9 import { useAppSelector } from "store/hooks"; 10 10 import { Redirect } from "components/aqlink"; 11 - import React, { useState } from "react"; 11 + import React, { useCallback, useState } from "react"; 12 12 import { useLiveUser } from "hooks/useLiveUser"; 13 13 import StreamKeyScreen from "components/live-dashboard/stream-key"; 14 14 ··· 24 24 const [streamSource, setStreamSource] = useState(StreamSource.Start); 25 25 const isLive = useLiveUser(); 26 26 const telemetry = useAppSelector(selectTelemetry); 27 + const videoRef = useCallback((node: HTMLVideoElement | null) => { 28 + console.log("videoRef", node); 29 + if (node !== null) { 30 + // save here for screenshot alter 31 + } 32 + }, []); 27 33 if (!isReady) { 28 34 return <Loading />; 29 35 } ··· 35 41 if (isWeb) { 36 42 params = new URLSearchParams(window.location.search); 37 43 } 44 + 38 45 if (isLive && streamSource !== StreamSource.Camera) { 39 46 topPane = ( 40 47 <Player 41 48 telemetry={telemetry === true} 42 49 src={userProfile.did} 43 50 name={userProfile.handle} 51 + videoRef={videoRef} 44 52 /> 45 53 ); 46 54 } else if (streamSource === StreamSource.Start) {