From eea05ab26ff85f876e2bd68d90595053e995b6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Wed, 11 Mar 2026 01:17:57 +0300 Subject: [PATCH] fix --- apps/web/src/components/CallModal.tsx | 181 +++++++++++++------------- 1 file changed, 87 insertions(+), 94 deletions(-) diff --git a/apps/web/src/components/CallModal.tsx b/apps/web/src/components/CallModal.tsx index 5eb3cc6..9a986ac 100644 --- a/apps/web/src/components/CallModal.tsx +++ b/apps/web/src/components/CallModal.tsx @@ -188,100 +188,6 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi setNoiseSuppression(false); }, []); - // Setup common peer connection event handlers - const setupPeerHandlers = useCallback((pc: RTCPeerConnection) => { - pc.onicecandidate = (e) => { - if (e.candidate) { - const socket = getSocket(); - socket?.emit('ice_candidate', { - targetUserId: targetUserIdRef.current, - candidate: e.candidate, - }); - } - }; - - pc.ontrack = (e) => { - console.log('[ontrack] Received track:', e.track.kind, 'readyState:', e.track.readyState, - 'enabled:', e.track.enabled, 'streams:', e.streams.length, - 'muted:', e.track.muted); - // Always merge new tracks into the existing remote stream to avoid losing - // audio when a video-only screen share track arrives on a separate stream. - let stream: MediaStream; - if (remoteStreamRef.current) { - if (!remoteStreamRef.current.getTracks().includes(e.track)) { - remoteStreamRef.current.addTrack(e.track); - } - stream = remoteStreamRef.current; - } else if (e.streams[0]) { - stream = e.streams[0]; - } else { - stream = new MediaStream([e.track]); - } - - remoteStreamRef.current = stream; - - // Check if remote has video tracks - const checkVideo = () => { - const videoTracks = stream.getVideoTracks(); - // hasRemoteVideo stays true if we have ANY live video track. - const hasVideo = videoTracks.length > 0 && videoTracks.some( - t => t.readyState === 'live' && t.enabled && !t.muted - ); - - console.log('[checkVideo] videoTracks:', videoTracks.map(t => - `${t.label} state=${t.readyState} enabled=${t.enabled} muted=${t.muted}` - ), '→ hasRemoteVideo:', hasVideo); - - setHasRemoteVideo(hasVideo); - // Fallback: if we detect a live video track but signalling hasn't updated us, switch to video mode - if (hasVideo && callType !== 'video') { - console.log('[checkVideo] Auto-switching to video mode'); - setCallType('video'); - } - }; - - // Listen for unmute/mute/ended on video tracks - const attachTrackListeners = (track: MediaStreamTrack) => { - if (track.kind !== 'video') return; - track.onunmute = checkVideo; - track.onmute = checkVideo; - track.onended = checkVideo; - }; - - stream.getVideoTracks().forEach(attachTrackListeners); - checkVideo(); - - if (remoteVideoRef.current) { - // Force re-assignment to ensure browser picks up new track - remoteVideoRef.current.srcObject = null; - remoteVideoRef.current.srcObject = stream; - } - if (remoteAudioRef.current) { - remoteAudioRef.current.srcObject = stream; - remoteAudioRef.current.play().catch(() => { }); - } - - // Track future additions/removals - stream.onaddtrack = (ev) => { - attachTrackListeners(ev.track); - checkVideo(); - }; - stream.onremovetrack = checkVideo; - - // Also schedule a delayed check — some browsers delay unmute - setTimeout(checkVideo, 500); - setTimeout(checkVideo, 1500); - }; - - pc.onconnectionstatechange = () => { - if (callEndedRef.current) return; - if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - endCallSafe(); - } - }; - }, []); - // Schedule a tracked close timeout const scheduleClose = useCallback((delay = 1500) => { if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current); @@ -303,6 +209,93 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi scheduleClose(); }, [cleanup, scheduleClose]); + // Setup common peer connection event handlers + const setupPeerHandlers = useCallback((pc: RTCPeerConnection) => { + pc.onicecandidate = (e) => { + if (e.candidate) { + const socket = getSocket(); + socket?.emit('ice_candidate', { + targetUserId: targetUserIdRef.current, + candidate: e.candidate, + }); + } + }; + + pc.ontrack = (event) => { + const track = event.track; + const streams = event.streams; + console.log(`[WebRTC] Received track: ${track.kind} state: ${track.readyState} streams: ${streams.length}`); + + // Helper to update video UI state + const checkVideo = () => { + const stream = remoteStreamRef.current; + if (!stream) { + setHasRemoteVideo(false); + return; + } + const videoTracks = stream.getVideoTracks(); + const hasVideo = videoTracks.length > 0 && videoTracks.some( + t => t.readyState === 'live' && t.enabled && !t.muted + ); + + console.log('[checkVideo] remote videoTracks:', videoTracks.map(t => + `${t.label} state=${t.readyState} enabled=${t.enabled} muted=${t.muted}` + ), '→ hasRemoteVideo:', hasVideo); + + setHasRemoteVideo(hasVideo); + if (hasVideo && callType !== 'video') { + console.log('[checkVideo] Auto-switching to video mode'); + setCallType('video'); + } + }; + + // Always merge tracks into a single remote stream + if (!remoteStreamRef.current) { + remoteStreamRef.current = streams[0] || new MediaStream([track]); + } else if (streams[0]) { + streams[0].getTracks().forEach(t => { + if (!remoteStreamRef.current?.getTracks().includes(t)) { + remoteStreamRef.current?.addTrack(t); + } + }); + } else if (!remoteStreamRef.current.getTracks().includes(track)) { + remoteStreamRef.current.addTrack(track); + } + + const stream = remoteStreamRef.current; + + // Bind to DOM elements + if (track.kind === 'video' && remoteVideoRef.current) { + remoteVideoRef.current.srcObject = stream; + remoteVideoRef.current.play().catch(err => console.warn('[WebRTC] video play failed:', err)); + checkVideo(); + } else if (track.kind === 'audio') { + if (remoteAudioRef?.current) { + remoteAudioRef.current.srcObject = stream; + remoteAudioRef.current.play().catch(err => console.warn('[WebRTC] audio play failed:', err)); + } else if (remoteVideoRef.current) { + remoteVideoRef.current.srcObject = stream; + } + } + + track.onunmute = checkVideo; + track.onmute = checkVideo; + track.onended = checkVideo; + + // Extra checks for slow browsers + setTimeout(checkVideo, 500); + setTimeout(checkVideo, 1500); + }; + + pc.onconnectionstatechange = () => { + console.log('[WebRTC] Connection state:', pc.connectionState); + if (callEndedRef.current) return; + if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { + endCallSafe(); + } + }; + }, [callType, endCallSafe]); + // Start outgoing call const startCall = useCallback(async () => { if (!targetUser) return;