import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, StatusBar, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Track, LocalVideoTrack, RemoteVideoTrack } from 'livekit-client'; let VideoView: React.ComponentType | null = null; try { VideoView = require('@livekit/react-native').VideoView; } catch { // WebRTC native module not available (e.g. Expo Go) } type VideoTrack = LocalVideoTrack | RemoteVideoTrack; import { callStore } from '../../stores/call'; import { liveKitService } from '@/services/livekit'; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; const CallScreen: React.FC = () => { const currentCall = callStore((s) => s.currentCall); const callDuration = callStore((s) => s.callDuration); const endCall = callStore((s) => s.endCall); const toggleMute = callStore((s) => s.toggleMute); const toggleSpeaker = callStore((s) => s.toggleSpeaker); const toggleVideo = callStore((s) => s.toggleVideo); const isMinimized = callStore((s) => s.isMinimized); const toggleMinimize = callStore((s) => s.toggleMinimize); const [remoteVideoTrack, setRemoteVideoTrack] = useState(null); const [localVideoTrack, setLocalVideoTrack] = useState(null); // Subscribe to LiveKit track events useEffect(() => { const unsubs: (() => void)[] = []; unsubs.push( liveKitService.on('trackSubscribed', ({ track }) => { if (track.kind === Track.Kind.Video) { setRemoteVideoTrack(track as VideoTrack); } }) ); unsubs.push( liveKitService.on('trackUnsubscribed', ({ track }) => { if (track.kind === Track.Kind.Video) { setRemoteVideoTrack(null); } }) ); unsubs.push( liveKitService.on('trackMuted', ({ publication }) => { if (publication.source === Track.Source.Camera) { setRemoteVideoTrack(null); } }) ); unsubs.push( liveKitService.on('trackUnmuted', ({ publication, participant }) => { if (publication.source === Track.Source.Camera && publication.track) { setRemoteVideoTrack(publication.track as VideoTrack); } }) ); // Poll local video track from localParticipant const localPollInterval = setInterval(() => { const lp = liveKitService.localParticipant; if (lp) { const videoPub = lp.getTrackPublication(Track.Source.Camera); const track = (videoPub?.track ?? null) as VideoTrack | null; setLocalVideoTrack((prev) => (prev === track ? prev : track)); } else { setLocalVideoTrack((prev) => (prev === null ? prev : null)); } }, 500); return () => { unsubs.forEach((unsub) => unsub()); clearInterval(localPollInterval); }; }, []); // Don't render full screen when minimized or no call if (!currentCall || isMinimized) return null; const getStatusText = (): string => { switch (currentCall.status) { case 'calling': return '正在等待对方接听...'; case 'ringing': return '来电响铃中...'; case 'connecting': return '连接中...'; case 'connected': return formatDuration(callDuration); case 'reconnecting': return '网络重连中...'; case 'ended': return '通话已结束'; case 'failed': return '连接失败'; default: return ''; } }; const handleEndCall = () => { endCall('hangup'); }; const showRemoteVideo = !!remoteVideoTrack; const showLocalVideo = currentCall?.isVideoEnabled && !!localVideoTrack; const isVideoCallActive = showRemoteVideo || showLocalVideo; return ( {/* Remote video - full screen */} {showRemoteVideo && remoteVideoTrack && VideoView && ( )} {/* Local video - picture in picture */} {showLocalVideo && localVideoTrack && VideoView && ( )} {/* Top area: minimize button */} {/* Center: Peer info - only show when no video */} {!isVideoCallActive && ( {currentCall.peerAvatar ? ( ) : ( {currentCall.peerName?.charAt(0).toUpperCase() || '?'} )} {currentCall.peerName || '未知用户'} {getStatusText()} )} {/* Peer name overlay when video is active */} {isVideoCallActive && ( {currentCall.peerName || '未知用户'} {getStatusText()} )} {/* Bottom controls */} {/* Mute */} {currentCall.isMuted ? '取消静音' : '静音'} {/* Video toggle */} {currentCall.isVideoEnabled ? '关闭摄像头' : '打开摄像头'} {/* Speaker */} {currentCall.isSpeakerOn ? '免提' : '听筒'} {/* End call */} 挂断 ); }; const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, backgroundColor: '#1A1A2E', zIndex: 9999, }, background: { ...StyleSheet.absoluteFillObject, backgroundColor: '#1A1A2E', }, topBar: { position: 'absolute', top: 50, left: 0, right: 0, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: 44, zIndex: 100, }, topButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255, 255, 255, 0.1)', justifyContent: 'center', alignItems: 'center', }, centerArea: { position: 'absolute', top: '28%', left: 0, right: 0, alignItems: 'center', paddingHorizontal: 30, }, avatarOuter: { marginBottom: 16, }, avatar: { width: 100, height: 100, borderRadius: 50, backgroundColor: '#3A3A5C', }, avatarPlaceholder: { justifyContent: 'center', alignItems: 'center', }, avatarText: { fontSize: 40, color: '#fff', fontWeight: '600', }, peerName: { fontSize: 22, fontWeight: '600', color: '#FFFFFF', marginBottom: 6, textAlign: 'center', maxWidth: 280, }, status: { fontSize: 14, color: 'rgba(255, 255, 255, 0.5)', letterSpacing: 0.3, }, fullScreenVideo: { ...StyleSheet.absoluteFillObject, backgroundColor: '#1A1A2E', }, localVideoContainer: { position: 'absolute', top: 100, right: 20, width: 120, height: 160, borderRadius: 16, overflow: 'hidden', backgroundColor: '#2A2A4E', borderWidth: 2, borderColor: 'rgba(255, 255, 255, 0.2)', zIndex: 50, }, localVideo: { width: '100%', height: '100%', }, videoOverlayInfo: { position: 'absolute', top: 100, left: 0, right: 0, alignItems: 'center', zIndex: 50, }, videoPeerName: { fontSize: 18, fontWeight: '600', color: '#FFFFFF', textShadowColor: 'rgba(0, 0, 0, 0.5)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, videoStatus: { fontSize: 14, color: 'rgba(255, 255, 255, 0.8)', marginTop: 4, textShadowColor: 'rgba(0, 0, 0, 0.5)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, controls: { position: 'absolute', bottom: 60, left: 0, right: 0, alignItems: 'center', zIndex: 100, }, controlRow: { flexDirection: 'row', justifyContent: 'center', gap: 24, marginBottom: 24, }, controlButton: { alignItems: 'center', width: 70, }, controlCircle: { width: 56, height: 56, borderRadius: 28, backgroundColor: 'rgba(255, 255, 255, 0.12)', justifyContent: 'center', alignItems: 'center', }, controlCircleActive: { backgroundColor: '#FFFFFF', }, controlButtonActive: {}, controlLabel: { fontSize: 11, color: 'rgba(255, 255, 255, 0.55)', marginTop: 8, textAlign: 'center', }, controlLabelActive: { color: '#FFFFFF', fontWeight: '500', }, endCallButton: { alignItems: 'center', }, endCallCircle: { width: 64, height: 64, borderRadius: 32, backgroundColor: '#E54D42', justifyContent: 'center', alignItems: 'center', }, endCallLabel: { fontSize: 11, color: 'rgba(255, 255, 255, 0.55)', marginTop: 8, }, }); export default CallScreen;