diff --git a/android/app/src/main/assets/public/voice.js b/android/app/src/main/assets/public/voice.js index b7ba7806..ea07e4c2 100644 --- a/android/app/src/main/assets/public/voice.js +++ b/android/app/src/main/assets/public/voice.js @@ -153,10 +153,6 @@ function cleanupPeerMedia(userId) { delete remoteAudio[aid]; delete audioDecryptChains[aid]; } - if (remoteOpusDecoders[aid]) { - try { remoteOpusDecoders[aid].close(); } catch (e) {} - delete remoteOpusDecoders[aid]; - } } for (const decoderId of Object.keys(remoteVideos)) { if (decoderId.split('-')[0] === userId) { @@ -647,8 +643,13 @@ setInterval(() => { if (others > 0 && now - lastVoiceServerPacketAt > VOICE_SERVER_TIMEOUT_MS) { console.warn("Voice: no inbound traffic, forcing reconnect"); try { currentVoiceWs.close(); } catch (e) {} - } else if (audioContext && audioContext.state === 'suspended') { + } else if (audioContext && audioContext.state !== 'running') { + //Nie tylko "suspended" - iOS/Safari zna też "interrupted"; każdy + //stan != running przy żywej rozmowie wymaga próby wznowienia. + //Dokładamy też kickstart-na-geście, jeśli jeszcze nie jest uzbrojony + //(armVoiceContextKickstart sam jest idempotentny) audioContext.resume().catch(() => {}); + armVoiceContextKickstart(); } // Safety net for the call-key handshake: if others are in the call but // we never received the root key (raced/lost signal), ask for it @@ -772,94 +773,37 @@ class TaskQueue { let voiceJoinInFlight = false; -// === S20: Opus (WebCodecs) dla mikrofonu z pełnym fallbackiem na legacy PCM === -// UWAGA: tymczasowo WYŁĄCZONE (raport użytkownika: cisza w VC po wdrożeniu - -// wymaga testów na realnych urządzeniach przed ponownym włączeniem). -// Flagą włącza się całość; ścieżka odbiorcza typu 6 jest bezpieczna i zostaje. -const ENABLE_VOICE_OPUS = false; -// Typ ramki 6 = opus (typ 0 pozostaje PCM). Layout wewnętrzny identyczny jak -// typ 0: [0]=typ, [1]=keyframe, [2..14)=IV, [14..)=ciphertext - serwer i tak -// dokleja clientId przed relayem. Brak AudioEncoder/AudioDecoder lub jakikolwiek -// błąd = powrót na surowe PCM (mieszane wersje klientów się nie wysypują). -let voiceOpusEncoder = null; -let voiceOpusEncoderFailed = false; -let voiceOpusInputTimestampUs = 0; -const remoteOpusDecoders = {}; - -function getVoiceOpusEncoder() { - if (!ENABLE_VOICE_OPUS || voiceOpusEncoderFailed) return null; - if (typeof window.AudioEncoder !== 'function') { voiceOpusEncoderFailed = true; return null; } - if (voiceOpusEncoder) return voiceOpusEncoder; - try { - voiceOpusEncoder = new AudioEncoder({ - output: (chunk) => { - try { - if (!currentVoiceWs || currentVoiceWs.readyState !== WebSocket.OPEN || !voiceMySenderCryptoKey) return; - const bytes = new Uint8Array(chunk.byteLength); - chunk.copyTo(bytes); - sendVoiceEncodedFrame(6, bytes); - } catch (e) {} - }, - error: () => { voiceOpusEncoderFailed = true; } - }); - voiceOpusEncoder.configure({ - codec: 'opus', - sampleRate: 48000, - numberOfChannels: 1, - bitrate: 32000 - }); - return voiceOpusEncoder; - } catch (e) { - voiceOpusEncoderFailed = true; - voiceOpusEncoder = null; - return null; +//Ograniczony log diagnostyczny toru głosu - pierwsze kilka wystąpień danego +//problemu trafia do konsoli, dalej cicho (brak spamu przy długich rozmowach) +let voiceErrorLogsLeft = 6; +function voiceLogError(where, err) { + if (voiceErrorLogsLeft > 0) { + voiceErrorLogsLeft--; + console.warn("Voice: error [" + where + "]", err); } } -async function sendVoiceEncodedFrame(type, plainBytes) { - //licznik IV inkrementowany synchronicznie PRZED awaitem - niezmiennik - //unikalności IV bez zmian - const iv = nextVoiceIv(); - const cipher = await crypto.subtle.encrypt({name: "AES-GCM", iv}, voiceMySenderCryptoKey, plainBytes); - if (!currentVoiceWs || currentVoiceWs.readyState !== WebSocket.OPEN) return; - const payload = new Uint8Array(14 + cipher.byteLength); - payload[0] = type; payload[1] = 1; payload.set(iv, 2); payload.set(new Uint8Array(cipher), 14); - currentVoiceWs.send(payload.buffer); -} - -function decodeVoiceOpus(audioId, targetNode, opusBuffer) { - try { - if (typeof window.AudioDecoder !== 'function') return; - let dec = remoteOpusDecoders[audioId]; - if (!dec) { - dec = new AudioDecoder({ - output: (audioData) => { - try { - const frames = audioData.numberOfFrames; - const f32 = new Float32Array(frames); - audioData.copyTo(f32, { planeIndex: 0, format: 'f32-planar' }); - const pcm = new Int16Array(frames); - for (let i = 0; i < frames; i++) { - const s = Math.max(-1, Math.min(1, f32[i])); - pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; - } - audioData.close(); - if (targetNode) targetNode.port.postMessage({ clientId: audioId, audioData: pcm.buffer }, [pcm.buffer]); - } catch (e) { try { audioData.close(); } catch (e2) {} } - }, - error: () => { delete remoteOpusDecoders[audioId]; } - }); - dec.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 1 }); - remoteOpusDecoders[audioId] = dec; - } - dec.decode(new EncodedAudioChunk({ - type: 'key', - timestamp: Math.trunc(performance.now() * 1000), - data: opusBuffer - })); - } catch (err) { - delete remoteOpusDecoders[audioId]; - } +//Wznawia AudioContext przy pierwszym geście użytkownika (polityka autoplay +//może zablokować kontekst stworzony po async-owych krokach dołączania do +//rozmowy). Listenery są one-shot i zdejmują się nawzajem. +let voiceCtxKickstartHandler = null; +function armVoiceContextKickstart() { + if (!audioContext) return; + if (voiceCtxKickstartHandler) return; //już uzbrojone + voiceCtxKickstartHandler = () => { + audioContext.resume({}).then(() => { + if (audioContext.state === 'running') { + console.warn("Voice: AudioContext resumed after user gesture"); + document.removeEventListener('click', voiceCtxKickstartHandler); + document.removeEventListener('touchend', voiceCtxKickstartHandler); + document.removeEventListener('keydown', voiceCtxKickstartHandler); + voiceCtxKickstartHandler = null; + } + }).catch(() => {}); + }; + document.addEventListener('click', voiceCtxKickstartHandler); + document.addEventListener('touchend', voiceCtxKickstartHandler); + document.addEventListener('keydown', voiceCtxKickstartHandler); } async function initiateVoiceCall(roomId = null) { @@ -1061,7 +1005,17 @@ async function startVoiceSession(serverUrl, ticket) { screenReceiverNode = new AudioWorkletNode(audioContext, 'audio-receiver'); isAudioWorkletLoaded = true; } - if (audioContext.state === 'suspended') await audioContext.resume(); + if (audioContext.state !== 'running') { + try { await audioContext.resume(); } catch (e) {} + } + if (audioContext.state !== 'running') { + //Polityka autoplay / stan "interrupted": kontekst nie wstał + //samoaktualizacyjnie. Bez tego cisza w OBIE strony (mikrofon + //i głośnik żyją w tym samym kontekście), mimo że wskaźniki + //mówienia i screen share działają (nie potrzebują kontekstu). + console.warn("Voice: AudioContext not running after open (" + audioContext.state + ") - arming user-gesture kickstart"); + armVoiceContextKickstart(); + } applyVoiceSpeakerSink(); setupVoiceOutput(); @@ -1107,30 +1061,6 @@ async function startVoiceSession(serverUrl, ticket) { if (vdsLevelFill) vdsLevelFill.style.backgroundColor = 'var(--big-green)'; if (!voiceMySenderCryptoKey) return; - - //S20: Opus gdy WebCodecs dostępne (~30x mniej bajtów niż PCM); - //każda awaria = spójny fallback na legacy PCM poniżej - const opusEnc = getVoiceOpusEncoder(); - if (opusEnc && opusEnc.state === 'configured') { - try { - const i16 = new Int16Array(audioData); - const f32 = new Float32Array(i16.length); - for (let i = 0; i < i16.length; i++) f32[i] = i16[i] / 32768; - const ad = new AudioData({ - format: 'f32-planar', - sampleRate: 48000, - numberOfFrames: i16.length, - numberOfChannels: 1, - timestamp: voiceOpusInputTimestampUs, - data: f32 - }); - voiceOpusInputTimestampUs += Math.round(i16.length * 1e6 / 48000); - opusEnc.encode(ad); - ad.close(); - return; - } catch (e) { /* fallback na PCM */ } - } - const iv = nextVoiceIv(); const cipher = await crypto.subtle.encrypt({name: "AES-GCM", iv}, voiceMySenderCryptoKey, audioData); const payload = new Uint8Array(14 + cipher.byteLength); @@ -1175,9 +1105,8 @@ async function startVoiceSession(serverUrl, ticket) { //podklucz nadawcy - deterministyczny z (root, clientId) const senderKeyPromise = getVoicePeerSenderKey(clientId); - if (type === 0 || type === 4 || type === 6) { + if (type === 0 || type === 4) { const isScreenAudio = (type === 4); - const isOpus = (type === 6); const audioId = isScreenAudio ? `${clientId}_screen` : clientId; if (!remoteAudio[audioId]) remoteAudio[audioId] = { lastUpdate: Date.now() }; remoteAudio[audioId].lastUpdate = Date.now(); @@ -1188,17 +1117,17 @@ async function startVoiceSession(serverUrl, ticket) { audioDecryptChains[audioId].enqueue(async () => { try { const senderKey = await senderKeyPromise; - if (!senderKey) return; - const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, senderKey, ciphertext); - if (isOpus) { - //S20: opus -> Int16 PCM -> ten sam jitter-buffer workletu - decodeVoiceOpus(audioId, targetReceiver, decryptedBuffer); - } else { - if(targetReceiver) targetReceiver.port.postMessage({ clientId: audioId, audioData: decryptedBuffer }, [decryptedBuffer]); + if (!senderKey) { + voiceLogError('media.nokey[' + audioId + ']', new Error('brak podklucza nadawcy - root nie zainstalowany?')); + return; } + const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, senderKey, ciphertext); + if(targetReceiver) targetReceiver.port.postMessage({ clientId: audioId, audioData: decryptedBuffer }, [decryptedBuffer]); setTileSpeaking(clientId, true, isScreenAudio ? `${clientId}-2` : null); - } catch (err) {} + } catch (err) { + voiceLogError('media.decrypt[' + audioId + ']', err); + } }); } else if (type === 1 || type === 2) { const decoderId = `${clientId}-${type}`; @@ -1388,6 +1317,15 @@ async function handleCallUpdate(data) { for (let u of state) { if (u !== myIdStr) sendVoiceSignalToUser(u, roomId); } + } else if (state.length > 0 && Date.now() - lastVoiceKeyRequestAt > 1500) { + //Jesteśmy w rozmowie BEZ klucza (np. dołączyliśmy do trwającej) - + //nie czekamy na tick watchdoga (do 4 s), prosimy natychmiast, + //inaczej pierwsze sekundy rozmowy są u nas i u nich ciszą + lastVoiceKeyRequestAt = Date.now(); + console.warn("Voice: joined keyless - requesting call key now"); + for (const u of state) { + if (u !== String(window.id)) sendVoiceSignalToUser(u, roomId, true); + } } updateVoiceUIUsers(); @@ -1586,6 +1524,7 @@ async function handleRoomSignal(data) { let aesKeyBytes = await deriveHybridKeyFromSetup(otherPublicKeysObj, setupData.ctMlKem); let decRoomKeyB64 = await decryptAesGcmFromBase64(setupData.enc, aesKeyBytes); await setVoiceRootKey(base64ToUint8(decRoomKeyB64)); + console.warn("Voice: call key installed (from peer " + signalObj.from + ")"); } } catch(e) { console.error("Signal decryption failed", e); @@ -2654,14 +2593,6 @@ function endVoiceSession() { lastVoiceKeyRequestAt = 0; voiceActiveUsers = []; voiceHadRemotePeers = false; - - //S20: teardown enkodera/dekoderów opus - if (voiceOpusEncoder) { try { voiceOpusEncoder.close(); } catch (e) {} voiceOpusEncoder = null; } - voiceOpusInputTimestampUs = 0; - for (const aid of Object.keys(remoteOpusDecoders)) { - try { remoteOpusDecoders[aid].close(); } catch (e) {} - delete remoteOpusDecoders[aid]; - } for (let id in audioDecoders) { try { audioDecoders[id].close(); } catch(e){} diff --git a/webroot/voice.js b/webroot/voice.js index b7ba7806..ea07e4c2 100644 --- a/webroot/voice.js +++ b/webroot/voice.js @@ -153,10 +153,6 @@ function cleanupPeerMedia(userId) { delete remoteAudio[aid]; delete audioDecryptChains[aid]; } - if (remoteOpusDecoders[aid]) { - try { remoteOpusDecoders[aid].close(); } catch (e) {} - delete remoteOpusDecoders[aid]; - } } for (const decoderId of Object.keys(remoteVideos)) { if (decoderId.split('-')[0] === userId) { @@ -647,8 +643,13 @@ setInterval(() => { if (others > 0 && now - lastVoiceServerPacketAt > VOICE_SERVER_TIMEOUT_MS) { console.warn("Voice: no inbound traffic, forcing reconnect"); try { currentVoiceWs.close(); } catch (e) {} - } else if (audioContext && audioContext.state === 'suspended') { + } else if (audioContext && audioContext.state !== 'running') { + //Nie tylko "suspended" - iOS/Safari zna też "interrupted"; każdy + //stan != running przy żywej rozmowie wymaga próby wznowienia. + //Dokładamy też kickstart-na-geście, jeśli jeszcze nie jest uzbrojony + //(armVoiceContextKickstart sam jest idempotentny) audioContext.resume().catch(() => {}); + armVoiceContextKickstart(); } // Safety net for the call-key handshake: if others are in the call but // we never received the root key (raced/lost signal), ask for it @@ -772,94 +773,37 @@ class TaskQueue { let voiceJoinInFlight = false; -// === S20: Opus (WebCodecs) dla mikrofonu z pełnym fallbackiem na legacy PCM === -// UWAGA: tymczasowo WYŁĄCZONE (raport użytkownika: cisza w VC po wdrożeniu - -// wymaga testów na realnych urządzeniach przed ponownym włączeniem). -// Flagą włącza się całość; ścieżka odbiorcza typu 6 jest bezpieczna i zostaje. -const ENABLE_VOICE_OPUS = false; -// Typ ramki 6 = opus (typ 0 pozostaje PCM). Layout wewnętrzny identyczny jak -// typ 0: [0]=typ, [1]=keyframe, [2..14)=IV, [14..)=ciphertext - serwer i tak -// dokleja clientId przed relayem. Brak AudioEncoder/AudioDecoder lub jakikolwiek -// błąd = powrót na surowe PCM (mieszane wersje klientów się nie wysypują). -let voiceOpusEncoder = null; -let voiceOpusEncoderFailed = false; -let voiceOpusInputTimestampUs = 0; -const remoteOpusDecoders = {}; - -function getVoiceOpusEncoder() { - if (!ENABLE_VOICE_OPUS || voiceOpusEncoderFailed) return null; - if (typeof window.AudioEncoder !== 'function') { voiceOpusEncoderFailed = true; return null; } - if (voiceOpusEncoder) return voiceOpusEncoder; - try { - voiceOpusEncoder = new AudioEncoder({ - output: (chunk) => { - try { - if (!currentVoiceWs || currentVoiceWs.readyState !== WebSocket.OPEN || !voiceMySenderCryptoKey) return; - const bytes = new Uint8Array(chunk.byteLength); - chunk.copyTo(bytes); - sendVoiceEncodedFrame(6, bytes); - } catch (e) {} - }, - error: () => { voiceOpusEncoderFailed = true; } - }); - voiceOpusEncoder.configure({ - codec: 'opus', - sampleRate: 48000, - numberOfChannels: 1, - bitrate: 32000 - }); - return voiceOpusEncoder; - } catch (e) { - voiceOpusEncoderFailed = true; - voiceOpusEncoder = null; - return null; +//Ograniczony log diagnostyczny toru głosu - pierwsze kilka wystąpień danego +//problemu trafia do konsoli, dalej cicho (brak spamu przy długich rozmowach) +let voiceErrorLogsLeft = 6; +function voiceLogError(where, err) { + if (voiceErrorLogsLeft > 0) { + voiceErrorLogsLeft--; + console.warn("Voice: error [" + where + "]", err); } } -async function sendVoiceEncodedFrame(type, plainBytes) { - //licznik IV inkrementowany synchronicznie PRZED awaitem - niezmiennik - //unikalności IV bez zmian - const iv = nextVoiceIv(); - const cipher = await crypto.subtle.encrypt({name: "AES-GCM", iv}, voiceMySenderCryptoKey, plainBytes); - if (!currentVoiceWs || currentVoiceWs.readyState !== WebSocket.OPEN) return; - const payload = new Uint8Array(14 + cipher.byteLength); - payload[0] = type; payload[1] = 1; payload.set(iv, 2); payload.set(new Uint8Array(cipher), 14); - currentVoiceWs.send(payload.buffer); -} - -function decodeVoiceOpus(audioId, targetNode, opusBuffer) { - try { - if (typeof window.AudioDecoder !== 'function') return; - let dec = remoteOpusDecoders[audioId]; - if (!dec) { - dec = new AudioDecoder({ - output: (audioData) => { - try { - const frames = audioData.numberOfFrames; - const f32 = new Float32Array(frames); - audioData.copyTo(f32, { planeIndex: 0, format: 'f32-planar' }); - const pcm = new Int16Array(frames); - for (let i = 0; i < frames; i++) { - const s = Math.max(-1, Math.min(1, f32[i])); - pcm[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; - } - audioData.close(); - if (targetNode) targetNode.port.postMessage({ clientId: audioId, audioData: pcm.buffer }, [pcm.buffer]); - } catch (e) { try { audioData.close(); } catch (e2) {} } - }, - error: () => { delete remoteOpusDecoders[audioId]; } - }); - dec.configure({ codec: 'opus', sampleRate: 48000, numberOfChannels: 1 }); - remoteOpusDecoders[audioId] = dec; - } - dec.decode(new EncodedAudioChunk({ - type: 'key', - timestamp: Math.trunc(performance.now() * 1000), - data: opusBuffer - })); - } catch (err) { - delete remoteOpusDecoders[audioId]; - } +//Wznawia AudioContext przy pierwszym geście użytkownika (polityka autoplay +//może zablokować kontekst stworzony po async-owych krokach dołączania do +//rozmowy). Listenery są one-shot i zdejmują się nawzajem. +let voiceCtxKickstartHandler = null; +function armVoiceContextKickstart() { + if (!audioContext) return; + if (voiceCtxKickstartHandler) return; //już uzbrojone + voiceCtxKickstartHandler = () => { + audioContext.resume({}).then(() => { + if (audioContext.state === 'running') { + console.warn("Voice: AudioContext resumed after user gesture"); + document.removeEventListener('click', voiceCtxKickstartHandler); + document.removeEventListener('touchend', voiceCtxKickstartHandler); + document.removeEventListener('keydown', voiceCtxKickstartHandler); + voiceCtxKickstartHandler = null; + } + }).catch(() => {}); + }; + document.addEventListener('click', voiceCtxKickstartHandler); + document.addEventListener('touchend', voiceCtxKickstartHandler); + document.addEventListener('keydown', voiceCtxKickstartHandler); } async function initiateVoiceCall(roomId = null) { @@ -1061,7 +1005,17 @@ async function startVoiceSession(serverUrl, ticket) { screenReceiverNode = new AudioWorkletNode(audioContext, 'audio-receiver'); isAudioWorkletLoaded = true; } - if (audioContext.state === 'suspended') await audioContext.resume(); + if (audioContext.state !== 'running') { + try { await audioContext.resume(); } catch (e) {} + } + if (audioContext.state !== 'running') { + //Polityka autoplay / stan "interrupted": kontekst nie wstał + //samoaktualizacyjnie. Bez tego cisza w OBIE strony (mikrofon + //i głośnik żyją w tym samym kontekście), mimo że wskaźniki + //mówienia i screen share działają (nie potrzebują kontekstu). + console.warn("Voice: AudioContext not running after open (" + audioContext.state + ") - arming user-gesture kickstart"); + armVoiceContextKickstart(); + } applyVoiceSpeakerSink(); setupVoiceOutput(); @@ -1107,30 +1061,6 @@ async function startVoiceSession(serverUrl, ticket) { if (vdsLevelFill) vdsLevelFill.style.backgroundColor = 'var(--big-green)'; if (!voiceMySenderCryptoKey) return; - - //S20: Opus gdy WebCodecs dostępne (~30x mniej bajtów niż PCM); - //każda awaria = spójny fallback na legacy PCM poniżej - const opusEnc = getVoiceOpusEncoder(); - if (opusEnc && opusEnc.state === 'configured') { - try { - const i16 = new Int16Array(audioData); - const f32 = new Float32Array(i16.length); - for (let i = 0; i < i16.length; i++) f32[i] = i16[i] / 32768; - const ad = new AudioData({ - format: 'f32-planar', - sampleRate: 48000, - numberOfFrames: i16.length, - numberOfChannels: 1, - timestamp: voiceOpusInputTimestampUs, - data: f32 - }); - voiceOpusInputTimestampUs += Math.round(i16.length * 1e6 / 48000); - opusEnc.encode(ad); - ad.close(); - return; - } catch (e) { /* fallback na PCM */ } - } - const iv = nextVoiceIv(); const cipher = await crypto.subtle.encrypt({name: "AES-GCM", iv}, voiceMySenderCryptoKey, audioData); const payload = new Uint8Array(14 + cipher.byteLength); @@ -1175,9 +1105,8 @@ async function startVoiceSession(serverUrl, ticket) { //podklucz nadawcy - deterministyczny z (root, clientId) const senderKeyPromise = getVoicePeerSenderKey(clientId); - if (type === 0 || type === 4 || type === 6) { + if (type === 0 || type === 4) { const isScreenAudio = (type === 4); - const isOpus = (type === 6); const audioId = isScreenAudio ? `${clientId}_screen` : clientId; if (!remoteAudio[audioId]) remoteAudio[audioId] = { lastUpdate: Date.now() }; remoteAudio[audioId].lastUpdate = Date.now(); @@ -1188,17 +1117,17 @@ async function startVoiceSession(serverUrl, ticket) { audioDecryptChains[audioId].enqueue(async () => { try { const senderKey = await senderKeyPromise; - if (!senderKey) return; - const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, senderKey, ciphertext); - if (isOpus) { - //S20: opus -> Int16 PCM -> ten sam jitter-buffer workletu - decodeVoiceOpus(audioId, targetReceiver, decryptedBuffer); - } else { - if(targetReceiver) targetReceiver.port.postMessage({ clientId: audioId, audioData: decryptedBuffer }, [decryptedBuffer]); + if (!senderKey) { + voiceLogError('media.nokey[' + audioId + ']', new Error('brak podklucza nadawcy - root nie zainstalowany?')); + return; } + const decryptedBuffer = await crypto.subtle.decrypt({name: "AES-GCM", iv}, senderKey, ciphertext); + if(targetReceiver) targetReceiver.port.postMessage({ clientId: audioId, audioData: decryptedBuffer }, [decryptedBuffer]); setTileSpeaking(clientId, true, isScreenAudio ? `${clientId}-2` : null); - } catch (err) {} + } catch (err) { + voiceLogError('media.decrypt[' + audioId + ']', err); + } }); } else if (type === 1 || type === 2) { const decoderId = `${clientId}-${type}`; @@ -1388,6 +1317,15 @@ async function handleCallUpdate(data) { for (let u of state) { if (u !== myIdStr) sendVoiceSignalToUser(u, roomId); } + } else if (state.length > 0 && Date.now() - lastVoiceKeyRequestAt > 1500) { + //Jesteśmy w rozmowie BEZ klucza (np. dołączyliśmy do trwającej) - + //nie czekamy na tick watchdoga (do 4 s), prosimy natychmiast, + //inaczej pierwsze sekundy rozmowy są u nas i u nich ciszą + lastVoiceKeyRequestAt = Date.now(); + console.warn("Voice: joined keyless - requesting call key now"); + for (const u of state) { + if (u !== String(window.id)) sendVoiceSignalToUser(u, roomId, true); + } } updateVoiceUIUsers(); @@ -1586,6 +1524,7 @@ async function handleRoomSignal(data) { let aesKeyBytes = await deriveHybridKeyFromSetup(otherPublicKeysObj, setupData.ctMlKem); let decRoomKeyB64 = await decryptAesGcmFromBase64(setupData.enc, aesKeyBytes); await setVoiceRootKey(base64ToUint8(decRoomKeyB64)); + console.warn("Voice: call key installed (from peer " + signalObj.from + ")"); } } catch(e) { console.error("Signal decryption failed", e); @@ -2654,14 +2593,6 @@ function endVoiceSession() { lastVoiceKeyRequestAt = 0; voiceActiveUsers = []; voiceHadRemotePeers = false; - - //S20: teardown enkodera/dekoderów opus - if (voiceOpusEncoder) { try { voiceOpusEncoder.close(); } catch (e) {} voiceOpusEncoder = null; } - voiceOpusInputTimestampUs = 0; - for (const aid of Object.keys(remoteOpusDecoders)) { - try { remoteOpusDecoders[aid].close(); } catch (e) {} - delete remoteOpusDecoders[aid]; - } for (let id in audioDecoders) { try { audioDecoders[id].close(); } catch(e){}