From f22e3b23b45db555590049373086a6b8c36507b4 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 13 Oct 2025 02:20:56 +0300 Subject: [PATCH] Clean up --- frontend/src/core/calls/e2eeWorker.ts | 62 +---- frontend/src/core/calls/webrtc.ts | 10 - frontend/src/pages/chat/css/_callWindow.scss | 10 +- frontend/src/pages/chat/hooks/useAudioCall.ts | 244 ------------------ .../chat/ui/right/MessagePanelRenderer.tsx | 4 +- 5 files changed, 9 insertions(+), 321 deletions(-) delete mode 100644 frontend/src/pages/chat/hooks/useAudioCall.ts diff --git a/frontend/src/core/calls/e2eeWorker.ts b/frontend/src/core/calls/e2eeWorker.ts index 6dac6c5..0feb9a5 100644 --- a/frontend/src/core/calls/e2eeWorker.ts +++ b/frontend/src/core/calls/e2eeWorker.ts @@ -34,7 +34,7 @@ export interface WorkerOptions { * For RTCEncodedVideoFrame/AudioFrame, we use the frame's metadata if available, * otherwise fall back to extracting from RTP header */ -function makeIV(encodedFrame: EncodedFrame, frameCount: number, mode: 'encrypt' | 'decrypt'): ArrayBuffer { +function makeIV(encodedFrame: EncodedFrame): ArrayBuffer { // Create IV using ONLY RTP metadata - this ensures sender and receiver use identical IVs // Frame data can differ between sender/receiver due to encoding differences const ivBuffer = new ArrayBuffer(12); @@ -49,14 +49,6 @@ function makeIV(encodedFrame: EncodedFrame, frameCount: number, mode: 'encrypt' view.setUint32(4, metadata.synchronizationSource || 0, false); // Middle 4 bytes view.setUint32(8, 0, false); // Last 4 bytes (padding for 12-byte IV) - // Debug first few frames only - if (frameCount <= 3) { - console.log(`${mode.toUpperCase()} IV for frame #${frameCount}:`, { - rtpTimestamp: metadata.rtpTimestamp, - syncSource: metadata.synchronizationSource, - mimeType: metadata.mimeType - }); - } return ivBuffer; } @@ -82,8 +74,6 @@ addEventListener("rtctransform", (event) => { console.log(`E2EE Worker started in ${mode.toUpperCase()} mode`); let frameCount = 0; - let lastLogTime = 0; - let lastKeyCheck = Date.now(); async function transform(encodedFrame: EncodedFrame, controller: TransformStreamDefaultController) { try { @@ -92,35 +82,8 @@ addEventListener("rtctransform", (event) => { // Increment frame counter frameCount++; - // Log every frame for debugging (only first 3) - if (frameCount <= 3) { - console.log(`${mode.toUpperCase()} Processing frame #${frameCount}, size: ${data.length}`); - } - // Create IV using RTP timestamp from metadata (synchronized between peers) - const iv = makeIV(encodedFrame, frameCount, mode); - - // Log first few frames and periodically for debugging - const now = Date.now(); - if (frameCount <= 5 || now - lastLogTime > 5000) { - console.log(`E2EE ${mode} frame #${frameCount}, size: ${data.length} bytes`); - lastLogTime = now; - } - - // For screen share, check if we need to request key rotation more frequently - // Screen share generates much more data and can benefit from more frequent key rotation - if (data.length > 50000 && now - lastKeyCheck > 60000) { // 1 minute for large frames - console.log("Large frame detected, suggesting key rotation for screen share"); - lastKeyCheck = now; - } - - // Detect potential browser window glitching - frames with specific characteristics - if (data.length > 100000 && frameCount > 10) { // Large frames after initial setup - const frameType = encodedFrame.type || 'unknown'; - if (frameType === 'key' && data.length > 200000) { - console.log("Large keyframe detected - possible browser window glitch, frame size:", data.length); - } - } + const iv = makeIV(encodedFrame); // Ensure IV is properly typed const ivArray = new Uint8Array(iv); @@ -168,16 +131,6 @@ addEventListener("rtctransform", (event) => { result.set(encryptedArray, 0); } - // Log first few frames for debugging - if (frameCount <= 3) { - console.log(`E2EE ${mode} frame #${frameCount}: ${data.length} -> ${result.length} bytes (header: ${headerSize})`); - } - - // For video frames, check if we need to force keyframes more frequently - // This helps prevent "stuck at first frame" issues with encrypted video - if (data.length > 10000 && frameCount > 0 && frameCount % 30 === 0) { - console.log(`Large video frame #${frameCount} - suggesting keyframe for stability`); - } // CRITICAL: Video frames need ArrayBuffer, not Uint8Array encodedFrame.data = result.buffer; @@ -187,16 +140,7 @@ addEventListener("rtctransform", (event) => { // FAIL SECURELY: Never send unencrypted frames const data = new Uint8Array(encodedFrame.data); console.error(`E2EE ${mode} FAILED - dropping frame #${frameCount}, size: ${data.length}`, e); - console.error('Frame type:', encodedFrame.type || 'unknown'); - - // For screen share, be more aggressive about dropping corrupted frames - // to prevent progressive glitch accumulation - if (mode === 'decrypt' && frameCount > 10) { - console.warn(`Dropping corrupted frame #${frameCount} to prevent glitch accumulation`); - } - - // Drop the frame completely - don't enqueue anything - return; + return; // Drop the frame completely } } diff --git a/frontend/src/core/calls/webrtc.ts b/frontend/src/core/calls/webrtc.ts index 49f2d9f..e18607c 100644 --- a/frontend/src/core/calls/webrtc.ts +++ b/frontend/src/core/calls/webrtc.ts @@ -109,7 +109,6 @@ async function getIceServers(): Promise { if (response.ok) { const data = await response.json() as IceServersResponse; - console.log("Received ICE servers:", data.iceServers); return data.iceServers || []; } else { console.warn("Failed to fetch ICE servers:", response.status, response.statusText); @@ -162,8 +161,6 @@ async function createPeerConnection(userId: number): Promise // Add ICE candidate event listener for debugging and sending peerConnection.addEventListener("icecandidate", async (event) => { if (event.candidate) { - console.log("Local ICE candidate:", event.candidate.candidate); - // Send ICE candidate to remote peer try { await sendSignalingMessage({ @@ -179,8 +176,6 @@ async function createPeerConnection(userId: number): Promise } catch (error) { console.error("Failed to send ICE candidate:", error); } - } else { - console.log("ICE gathering complete"); } }); @@ -195,27 +190,22 @@ async function createPeerConnection(userId: number): Promise // Handle renegotiation when tracks are added/removed peerConnection.addEventListener("negotiationneeded", async () => { try { - console.log("Negotiation needed for user", userId); const call = calls.get(userId); if (!call) { - console.log("Skipping renegotiation - call not found"); return; } // Prevent multiple simultaneous negotiations if (call.isNegotiating) { - console.log("Already negotiating, skipping"); return; } // Skip if we're in "stable" state and haven't finished the initial handshake if (peerConnection.signalingState !== "stable") { - console.log("Skipping renegotiation - signaling state is", peerConnection.signalingState); return; } call.isNegotiating = true; - console.log("Creating new offer for renegotiation (signalingState:", peerConnection.signalingState + ")"); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); diff --git a/frontend/src/pages/chat/css/_callWindow.scss b/frontend/src/pages/chat/css/_callWindow.scss index ed58fd8..b54e2ab 100644 --- a/frontend/src/pages/chat/css/_callWindow.scss +++ b/frontend/src/pages/chat/css/_callWindow.scss @@ -150,14 +150,12 @@ } } - .screen-share-tile { - // min-height removed to allow dynamic sizing based on video content - - .screen-share-video { - object-fit: contain; + .screen-share-tile { + .screen-share-video { + object-fit: contain; + } } } - } .call-controls { padding: 8px; diff --git a/frontend/src/pages/chat/hooks/useAudioCall.ts b/frontend/src/pages/chat/hooks/useAudioCall.ts deleted file mode 100644 index 8c54444..0000000 --- a/frontend/src/pages/chat/hooks/useAudioCall.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { useAppState } from "@/pages/chat/state"; -import * as WebRTC from "@/core/calls/webrtc"; -import { CallSignalingHandler } from "@/core/calls/signaling"; -import { setCallSignalingHandler } from "@/core/websocket"; -import { generateCallSessionKey, generateCallEmojis } from "@/core/calls/encryption"; -import { createRef, useEffect } from "react"; - -// Global audio ref shared across all instances -let globalRemoteAudioRef = createRef(); - -export default function useAudioCall() { - const { chat, startCall, endCall, setCallStatus, toggleMute, setCallEncryption, setCallSessionKeyHash, user } = useAppState(); - const remoteAudioRef = globalRemoteAudioRef; - - useEffect(() => { - if (user.authToken) { - WebRTC.setAuthToken(user.authToken); - } - - // Initialize call signaling handler - const signalingHandler = new CallSignalingHandler(() => ({ - receiveCall: (userId: number, username: string) => { - // Use the receiveCall function from state - const state = useAppState.getState(); - state.receiveCall(userId, username); - }, - endCall, - setCallSessionKeyHash, - setRemoteVideoEnabled: (enabled: boolean) => { - const state = useAppState.getState(); - state.setRemoteVideoEnabled(enabled); - }, - setRemoteScreenSharing: (enabled: boolean) => { - const state = useAppState.getState(); - state.setRemoteScreenSharing(enabled); - } - })); - setCallSignalingHandler(signalingHandler); - - // Set up call state change handler - WebRTC.setCallStateChangeHandler((userId: number, state: string) => { - const call = chat.call; - if (call.remoteUserId === userId) { - switch (state) { - case "connecting": - setCallStatus("connecting"); - break; - case "connected": - setCallStatus("active"); - break; - case "disconnected": - case "failed": - case "closed": - endCall(); - break; - } - } - }); - - // Set up remote stream handler - WebRTC.setRemoteStreamHandler((_userId: number, stream: MediaStream) => { - if (!remoteAudioRef.current) { - return; - } - const el = remoteAudioRef.current; - try { - el.srcObject = stream; - el.muted = false; - el.volume = 1.0; - el.autoplay = true; - - // Handle audio events - el.addEventListener("error", () => { - console.warn("[AUDIO] element error", (el.error?.message) || el.error); - }); - - el.play().catch(() => { - // Try to play after user interaction if autoplay is blocked - const playAfterInteraction = () => { - el.play().catch(() => {}); - document.removeEventListener('click', playAfterInteraction); - document.removeEventListener('touchstart', playAfterInteraction); - }; - document.addEventListener('click', playAfterInteraction); - document.addEventListener('touchstart', playAfterInteraction); - }); - } catch (e) { - console.warn("failed to attach remote stream:", e); - } - }); - - return () => { - WebRTC.cleanup(); - setCallSignalingHandler(null); - }; - }, [user.authToken, chat.call.remoteUserId, setCallStatus, endCall, startCall]); - - // Watch for session key hash changes and generate emojis - useEffect(() => { - if (chat.call.sessionKeyHash && chat.call.encryptionEmojis.length === 0) { - const emojis = generateCallEmojis(chat.call.sessionKeyHash); - setCallEncryption(chat.call.sessionKeyHash, emojis); - } - }, [chat.call.sessionKeyHash, chat.call.encryptionEmojis.length, setCallEncryption]); - - async function requestAudioPermissions(): Promise { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: false - }); - - // Stop the stream immediately as we just needed permission - stream.getTracks().forEach(track => track.stop()); - return true; - } catch (error) { - console.error("Failed to get audio permissions:", error); - return false; - } - }; - - async function initiateCall(userId: number, username: string) { - const hasPermission = await requestAudioPermissions(); - - if (!hasPermission) { - console.log("Audio permission denied"); - return; - } - - - let sessionKey; - try { - // Generate call session key and emojis - sessionKey = await generateCallSessionKey(); - const emojis = generateCallEmojis(sessionKey.hash); - - // Start the call in state - startCall(userId, username); - setCallStatus("calling"); - setCallEncryption(sessionKey.hash, emojis); - } catch (error) { - console.error("Failed to generate call encryption:", error); - endCall(); - return; - } - - // Initiate WebRTC call - const success = await WebRTC.initiateCall(userId, username); - - if (success && sessionKey) { - // Set the session key for ourselves (initiator) - await WebRTC.setSessionKey(userId, sessionKey.key); - - // Send session key hash to the receiver for visual verification - await WebRTC.sendCallSessionKey(userId, sessionKey.hash); - // Also wrap and send the actual session key for E2EE media - await WebRTC.sendWrappedCallSessionKey(userId, sessionKey.key, sessionKey.hash); - } else { - endCall(); - } - }; - - async function acceptCall() { - if (!chat.call.remoteUserId) { - return; - } - - setCallStatus("connecting"); - const success = await WebRTC.acceptCall(chat.call.remoteUserId); - - if (!success) { - endCall(); - } - }; - - async function rejectCall() { - if (!chat.call.remoteUserId) { - return; - } - - await WebRTC.rejectCall(chat.call.remoteUserId); - endCall(); - }; - - async function handleEndCall() { - if (chat.call.remoteUserId) { - await WebRTC.endCall(chat.call.remoteUserId); - } - endCall(); - }; - - function handleToggleMute() { - if (chat.call.remoteUserId) { - const isMuted = WebRTC.toggleMute(chat.call.remoteUserId); - // Update mute state in store - if (isMuted !== chat.call.isMuted) { - toggleMute(); - } - } - }; - - async function handleIncomingCall(userId: number, username: string) { - // Don't generate session key here - wait for it from the caller - await WebRTC.handleIncomingCall(userId, username); - }; - - async function handleCallOffer(userId: number, offer: RTCSessionDescriptionInit) { - await WebRTC.handleCallOffer(userId, offer); - }; - - async function handleCallAnswer(userId: number, answer: RTCSessionDescriptionInit) { - await WebRTC.handleCallAnswer(userId, answer); - }; - - async function handleIceCandidate(userId: number, candidate: RTCIceCandidateInit) { - await WebRTC.handleIceCandidate(userId, candidate); - }; - - async function handleCallSessionKey(sessionKeyHash: string) { - try { - // Just generate and display the emojis from the hash - // The actual session key will arrive via the wrapped key mechanism - const emojis = generateCallEmojis(sessionKeyHash); - setCallEncryption(sessionKeyHash, emojis); - } catch (error) { - console.error("Failed to generate call emojis from hash:", error); - } - }; - - return { - call: chat.call, - initiateCall, - acceptCall, - rejectCall, - endCall: handleEndCall, - toggleMute: handleToggleMute, - handleIncomingCall, - handleCallOffer, - handleCallAnswer, - handleIceCandidate, - handleCallSessionKey, - remoteAudioRef - }; -} diff --git a/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx index 48a0b68..b441f5b 100644 --- a/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx +++ b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx @@ -8,7 +8,7 @@ import type { Message, WebSocketMessage } from "@/core/types"; import defaultAvatar from "@/images/default-avatar.png"; import AnimatedOpacity from "@/core/components/animations/AnimatedOpacity"; import type { DMPanel } from "./panels/DMPanel"; -import useAudioCall from "@/pages/chat/hooks/useAudioCall"; +import useCall from "@/pages/chat/hooks/useCall"; interface MessagePanelRendererProps { panel: MessagePanel | null; @@ -27,7 +27,7 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) { const [editMessage, setEditMessage] = useState(null); const [editVisible, setEditVisible] = useState(Boolean(editMessage)); const [pendingAction, setPendingAction] = useState(null); - const { initiateCall } = useAudioCall(); + const { initiateCall } = useCall(); // Drag & drop