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 224 playerId, 225 225 ); 226 226 const isIngesting = usePlayerStore((x) => x.ingestConnectionState !== null); 227 + const pipAction = usePlayerStore((x) => x.pipAction); 227 228 228 229 const fadeAnim = useRef(new Animated.Value(1)).current; 229 230 ··· 248 249 if (isRefObject(videoRef)) { 249 250 video = videoRef.current; 250 251 } 251 - if (video) { 252 - setPipSupported( 253 - !!document.pictureInPictureEnabled && 254 - typeof video.requestPictureInPicture === "function", 255 - ); 256 - } else { 257 - setPipSupported(false); 258 - } 252 + setPipSupported( 253 + !!document.pictureInPictureEnabled && pipAction !== undefined, 254 + ); 259 255 }, [videoRef]); 260 256 261 257 useEffect(() => { ··· 281 277 }, [videoRef]); 282 278 283 279 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 - }); 280 + if (pipAction) pipAction(); 292 281 }, [videoRef]); 293 282 294 283 const userInteraction = () => {
+40 -17
js/app/components/player/video.tsx
··· 59 59 }; 60 60 61 61 const VideoElement = forwardRef( 62 - (props: VideoProps, ref: ForwardedRef<HTMLVideoElement | null>) => { 62 + (props: VideoProps, refCallback: ForwardedRef<HTMLVideoElement | null>) => { 63 63 const x = usePlayerStore((x) => x); 64 64 const url = useStreamplaceStore((x) => x.url); 65 65 const playerEvent = usePlayerStore((x) => x.playerEvent); ··· 84 84 85 85 const localVideoRef = useRef<HTMLVideoElement | null>(null); 86 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 + 87 124 // attempts to autoplay the video. if that fails, it attempts 88 125 // to play the video muted; some browsers will only let you 89 126 // autoplay if you're muted ··· 115 152 return () => { 116 153 setStatus(PlayerStatus.START); 117 154 }; 118 - }, []); 155 + }, [setStatus]); 119 156 120 157 useEffect(() => { 121 158 if (localVideoRef.current) { ··· 123 160 console.log("Setting volume to", volume); 124 161 } 125 162 }, [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 163 141 164 return ( 142 165 <View ··· 356 379 } 357 380 if (typeof videoRef === "function") { 358 381 videoRef(node); 359 - } else if (videoRef) { 382 + } else if (setVideoRef) { 360 383 setVideoRef(localVideoRef); 361 384 } 362 385 }, []);
+4
js/components/src/player-store/player-state.tsx
··· 119 119 | undefined, 120 120 ) => void; 121 121 122 + pipAction: (() => void) | undefined; 123 + /** Function to set the Picture-in-Picture action */ 124 + setPipAction: (action: (() => void) | undefined) => void; 125 + 122 126 /** Player element width (CSS value or number) */ 123 127 playerWidth?: string | number; 124 128 /** Function to set the player width */
+5
js/components/src/player-store/player-store.tsx
··· 83 83 pipMode: false, 84 84 setPipMode: (pipMode: boolean) => set(() => ({ pipMode })), 85 85 86 + // Picture-in-Picture action function (set by player component) 87 + pipAction: undefined, 88 + setPipAction: (action: (() => void) | undefined) => 89 + set(() => ({ pipAction: action })), 90 + 86 91 // Player element width/height setters for global sync 87 92 playerWidth: undefined, 88 93 setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })),