Live video on the AT Protocol

Refactor PiP handling to use pipAction from player store

+54 -33
+5 -16
js/app/components/player/controls.tsx
··· 224 playerId, 225 ); 226 const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null); 227 228 const fadeAnim = useRef(new Animated.Value(1)).current; 229 ··· 248 if (isRefObject(videoRef)) { 249 video = videoRef.current; 250 } 251 - if (video) { 252 - setPipSupported( 253 - !!document.pictureInPictureEnabled && 254 - typeof video.requestPictureInPicture === "function", 255 - ); 256 - } else { 257 - setPipSupported(false); 258 - } 259 }, [videoRef]); 260 261 useEffect(() => { ··· 281 }, [videoRef]); 282 283 const handlePip = useCallback(() => { 284 - let video: HTMLVideoElement | null = null; 285 - if (isRefObject(videoRef)) { 286 - video = videoRef.current; 287 - } 288 - if (!video) return; 289 - video.requestPictureInPicture().catch((err) => { 290 - console.error("Failed to enter Picture-in-Picture mode", err); 291 - }); 292 }, [videoRef]); 293 294 const userInteraction = () => {
··· 224 playerId, 225 ); 226 const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null); 227 + const pipAction = usePlayerStore((x) => x.pipAction); 228 229 const fadeAnim = useRef(new Animated.Value(1)).current; 230 ··· 249 if (isRefObject(videoRef)) { 250 video = videoRef.current; 251 } 252 + setPipSupported( 253 + !!document.pictureInPictureEnabled && pipAction !== undefined, 254 + ); 255 }, [videoRef]); 256 257 useEffect(() => { ··· 277 }, [videoRef]); 278 279 const handlePip = useCallback(() => { 280 + if (pipAction) pipAction(); 281 }, [videoRef]); 282 283 const userInteraction = () => {
+40 -17
js/app/components/player/video.tsx
··· 59 }; 60 61 const VideoElement = forwardRef( 62 - (props: VideoProps, ref: ForwardedRef<HTMLVideoElement | null>) => { 63 const x = usePlayerStore((x) => x); 64 const url = useStreamplaceStore((x) => x.url); 65 const playerEvent = usePlayerStore((x) => x.playerEvent); ··· 84 85 const localVideoRef = useRef<HTMLVideoElement | null>(null); 86 87 // attempts to autoplay the video. if that fails, it attempts 88 // to play the video muted; some browsers will only let you 89 // autoplay if you're muted ··· 115 return () => { 116 setStatus(PlayerStatus.START); 117 }; 118 - }, []); 119 120 useEffect(() => { 121 if (localVideoRef.current) { ··· 123 console.log("Setting volume to", volume); 124 } 125 }, [volume]); 126 - 127 - // Use a callback ref to handle when the video element is mounted 128 - const handleVideoRef = (videoElement: HTMLVideoElement | null) => { 129 - if (videoElement && typeof ref === "function") { 130 - ref(videoElement); 131 - } else if (videoElement && ref && "current" in ref) { 132 - ref.current = videoElement; 133 - } 134 - 135 - // Additional initialization can be done here when the video element is first mounted 136 - if (videoElement) { 137 - localVideoRef.current = videoElement; 138 - } 139 - }; 140 141 return ( 142 <View ··· 356 } 357 if (typeof videoRef === "function") { 358 videoRef(node); 359 - } else if (videoRef) { 360 setVideoRef(localVideoRef); 361 } 362 }, []);
··· 59 }; 60 61 const VideoElement = forwardRef( 62 + (props: VideoProps, refCallback: ForwardedRef<HTMLVideoElement | null>) => { 63 const x = usePlayerStore((x) => x); 64 const url = useStreamplaceStore((x) => x.url); 65 const playerEvent = usePlayerStore((x) => x.playerEvent); ··· 84 85 const localVideoRef = useRef<HTMLVideoElement | null>(null); 86 87 + // setPipAction comes from Zustand store 88 + useEffect(() => { 89 + if (typeof x.setPipAction === "function") { 90 + const fn = () => { 91 + if (localVideoRef.current) { 92 + try { 93 + localVideoRef.current.requestPictureInPicture?.(); 94 + } catch (err) { 95 + console.error("Error requesting Picture-in-Picture:", err); 96 + } 97 + } else { 98 + console.log("No video ref available for PiP"); 99 + } 100 + }; 101 + x.setPipAction(fn); 102 + } 103 + // Cleanup on unmount 104 + return () => { 105 + if (typeof x.setPipAction === "function") { 106 + x.setPipAction(undefined); 107 + } 108 + }; 109 + }, []); 110 + 111 + // Memoized callback ref for video element 112 + const handleVideoRef = useCallback( 113 + (videoElement: HTMLVideoElement | null) => { 114 + localVideoRef.current = videoElement; 115 + if (typeof refCallback === "function") { 116 + refCallback(videoElement); 117 + } else if (refCallback && "current" in refCallback) { 118 + refCallback.current = videoElement; 119 + } 120 + }, 121 + [refCallback], 122 + ); 123 + 124 // attempts to autoplay the video. if that fails, it attempts 125 // to play the video muted; some browsers will only let you 126 // autoplay if you're muted ··· 152 return () => { 153 setStatus(PlayerStatus.START); 154 }; 155 + }, [setStatus]); 156 157 useEffect(() => { 158 if (localVideoRef.current) { ··· 160 console.log("Setting volume to", volume); 161 } 162 }, [volume]); 163 164 return ( 165 <View ··· 379 } 380 if (typeof videoRef === "function") { 381 videoRef(node); 382 + } else if (setVideoRef) { 383 setVideoRef(localVideoRef); 384 } 385 }, []);
+4
js/components/src/player-store/player-state.tsx
··· 119 | undefined, 120 ) => void; 121 122 /** Player element width (CSS value or number) */ 123 playerWidth?: string | number; 124 /** Function to set the player width */
··· 119 | undefined, 120 ) => void; 121 122 + pipAction: (() => void) | undefined; 123 + /** Function to set the Picture-in-Picture action */ 124 + setPipAction: (action: (() => void) | undefined) => void; 125 + 126 /** Player element width (CSS value or number) */ 127 playerWidth?: string | number; 128 /** Function to set the player width */
+5
js/components/src/player-store/player-store.tsx
··· 83 pipMode: false, 84 setPipMode: (pipMode: boolean) => set(() => ({ pipMode })), 85 86 // Player element width/height setters for global sync 87 playerWidth: undefined, 88 setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),
··· 83 pipMode: false, 84 setPipMode: (pipMode: boolean) => set(() => ({ pipMode })), 85 86 + // Picture-in-Picture action function (set by player component) 87 + pipAction: undefined, 88 + setPipAction: (action: (() => void) | undefined) => 89 + set(() => ({ pipAction: action })), 90 + 91 // Player element width/height setters for global sync 92 playerWidth: undefined, 93 setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),