import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, type RefObject, } from 'react'; type ReactNative = typeof import('react-native'); function rn(): ReactNative { return require('react-native') as ReactNative; } type NitroModules = typeof import('react-native-nitro-modules'); function nitro(): NitroModules { return require('react-native-nitro-modules') as NitroModules; } import type { PlaybackEngine as PlaybackEngineHybrid } from '../../specs/PlaybackEngine.nitro'; import type { FullscreenConfig } from '../types'; import type { VideoView as NitroVideoView } from '../../specs/VideoView.nitro'; import type { VideoViewRef } from './VideoView'; import { createVideoViewAttachmentState, detachVideoView, syncVideoViewAttachment, } from './videoViewAttachment'; const makeNoopCallback = () => nitro().callback(() => {}); type NitroVideoViewWithPipProbe = NitroVideoView & { isPipSupported?: () => boolean; }; function hasPipProbe( view: NitroVideoView | null ): view is NitroVideoViewWithPipProbe & { isPipSupported: () => boolean } { return ( view != null && typeof (view as NitroVideoViewWithPipProbe).isPipSupported === 'function' ); } interface UseVideoViewCallbacksArgs { engine: PlaybackEngineHybrid | undefined; onPipStart?: () => void; onPipStop?: () => void; onFullscreenEnter?: () => void; onFullscreenExit?: () => void; videoRef?: RefObject; } export function useVideoViewCallbacks(args: UseVideoViewCallbacksArgs) { const { engine, onPipStart, onPipStop, onFullscreenEnter, onFullscreenExit, videoRef, } = args; const hybridViewRef = useRef(null); const attachmentRef = useRef( createVideoViewAttachmentState() ); const detachAttachedView = useCallback(() => { detachVideoView(attachmentRef.current); }, []); const attachCurrentView = useCallback( (engineToAttach: PlaybackEngineHybrid | undefined) => { syncVideoViewAttachment( attachmentRef.current, hybridViewRef.current, engineToAttach ); }, [] ); useEffect(() => { attachCurrentView(engine); }, [attachCurrentView, engine]); useEffect( () => () => { detachAttachedView(); hybridViewRef.current = null; }, [detachAttachedView] ); // Expose the imperative API via useImperativeHandle instead of writing // videoRef.current during render. The handle only reads hybridViewRef (a // stable ref) at call time, so it never needs to be rebuilt; React assigns // it on mount and clears it on unmount. useImperativeHandle( videoRef, () => ({ startPictureInPicture() { hybridViewRef.current?.startPictureInPicture(); }, stopPictureInPicture() { hybridViewRef.current?.stopPictureInPicture(); }, enterFullscreen(config?: FullscreenConfig) { hybridViewRef.current?.enterFullscreen(config); }, exitFullscreen() { hybridViewRef.current?.exitFullscreen(); }, isFullscreen() { return hybridViewRef.current?.isFullscreen() ?? false; }, enterNativeFullscreen() { hybridViewRef.current?.enterNativeFullscreen(); }, isPipSupported() { if (rn().Platform.OS === 'ios') return true; const ref = hybridViewRef.current; return hasPipProbe(ref) ? ref.isPipSupported() : false; }, }), [] ); // Memoize callback wrappers; without this every re-render (e.g. useMediaPosition at 10Hz) // creates new refs → Fabric updateProps loop. const pipStartCb = useMemo( () => (onPipStart ? nitro().callback(onPipStart) : makeNoopCallback()), [onPipStart] ); const pipStopCb = useMemo( () => (onPipStop ? nitro().callback(onPipStop) : makeNoopCallback()), [onPipStop] ); const fullscreenEnterCb = useMemo( () => (onFullscreenEnter ? nitro().callback(onFullscreenEnter) : makeNoopCallback()), [onFullscreenEnter] ); const fullscreenExitCb = useMemo( () => (onFullscreenExit ? nitro().callback(onFullscreenExit) : makeNoopCallback()), [onFullscreenExit] ); const hybridRefCb = useMemo( () => nitro().callback((ref: NitroVideoView | null) => { if (hybridViewRef.current != null && hybridViewRef.current !== ref) { detachAttachedView(); } hybridViewRef.current = ref; attachCurrentView(engine); }), [attachCurrentView, detachAttachedView, engine] ); return { pipStartCb, pipStopCb, fullscreenEnterCb, fullscreenExitCb, hybridRefCb, }; }