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