import { useCallback, useEffect, useRef, useState } from 'react'; import { Button, LayoutChangeEvent, StyleSheet, View } from 'react-native'; import type { WebView } from 'react-native-webview'; import type { WebViewMessageEvent } from 'react-native-webview'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { LoadingSpinner } from '@/components/LoadingSpinner'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; import { usePoseStream } from '@/hooks/usePoseStream'; import { colors, spacing } from '@/theme'; import type { ConnectionStatus, SensingFrame } from '@/types/sensing'; import { useGaussianBridge } from './useGaussianBridge'; import { GaussianSplatWebView } from './GaussianSplatWebView'; import { LiveHUD } from './LiveHUD'; type LiveMode = 'LIVE' | 'SIM' | 'RSSI'; const getMode = ( status: ConnectionStatus, isSimulated: boolean, frame: SensingFrame | null, ): LiveMode => { if (isSimulated || frame?.source === 'simulated') { return 'SIM'; } if (status === 'connected') { return 'LIVE'; } return 'RSSI'; }; const dispatchWebViewMessage = (webViewRef: { current: WebView | null }, message: unknown) => { const webView = webViewRef.current; if (!webView) { return; } const payload = JSON.stringify(message); webView.injectJavaScript( `window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify(payload)} })); true;`, ); }; export const LiveScreen = () => { const webViewRef = useRef(null); const { lastFrame, connectionStatus, isSimulated } = usePoseStream(); const bridge = useGaussianBridge(webViewRef); const [webError, setWebError] = useState(null); const [viewerKey, setViewerKey] = useState(0); const sendTimeoutRef = useRef | null>(null); const pendingFrameRef = useRef(null); const lastSentAtRef = useRef(0); const clearSendTimeout = useCallback(() => { if (!sendTimeoutRef.current) { return; } clearTimeout(sendTimeoutRef.current); sendTimeoutRef.current = null; }, []); useEffect(() => { if (!lastFrame) { return; } pendingFrameRef.current = lastFrame; const now = Date.now(); const flush = () => { if (!bridge.isReady || !pendingFrameRef.current) { return; } bridge.sendFrame(pendingFrameRef.current); lastSentAtRef.current = Date.now(); pendingFrameRef.current = null; }; const waitMs = Math.max(0, 500 - (now - lastSentAtRef.current)); if (waitMs <= 0) { flush(); return; } clearSendTimeout(); sendTimeoutRef.current = setTimeout(() => { sendTimeoutRef.current = null; flush(); }, waitMs); return () => { clearSendTimeout(); }; }, [bridge.isReady, lastFrame, bridge.sendFrame, clearSendTimeout]); useEffect(() => { return () => { dispatchWebViewMessage(webViewRef, { type: 'DISPOSE' }); clearSendTimeout(); pendingFrameRef.current = null; }; }, [clearSendTimeout]); const onMessage = useCallback( (event: WebViewMessageEvent) => { bridge.onMessage(event); }, [bridge], ); const onLayout = useCallback((event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout; if (width <= 0 || height <= 0 || Number.isNaN(width) || Number.isNaN(height)) { return; } dispatchWebViewMessage(webViewRef, { type: 'RESIZE', payload: { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)), }, }); }, []); const handleWebError = useCallback(() => { setWebError('Live renderer failed to initialize'); }, []); const handleRetry = useCallback(() => { setWebError(null); bridge.reset(); setViewerKey((value) => value + 1); }, [bridge]); const rssi = lastFrame?.features?.mean_rssi; const personCount = lastFrame?.classification?.presence ? 1 : 0; const mode = getMode(connectionStatus, isSimulated, lastFrame); if (webError || bridge.error) { return ( Live visualization failed {webError ?? bridge.error}