diff --git a/shine-UI/js/services/call-service.js b/shine-UI/js/services/call-service.js index 767ff8e..c96d82b 100644 --- a/shine-UI/js/services/call-service.js +++ b/shine-UI/js/services/call-service.js @@ -135,8 +135,8 @@ function getCallStateSnapshot() { muted: Boolean(call.muted), canAnswer: callPhase === 'incoming', canDecline: callPhase === 'incoming', - canHangup: callPhase !== 'ended', - canMute: callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing', + canHangup: callPhase !== 'ended' && callPhase !== 'incoming', + canMute: callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing' || callPhase === 'reconnecting', }; } @@ -177,6 +177,76 @@ async function closeMedia(call) { call.localStream = null; } +function stopReconnectFlow(call) { + if (!call?.timers) return; + if (call.timers.reconnectStep) { + clearTimeout(call.timers.reconnectStep); + call.timers.reconnectStep = null; + } + if (call.timers.reconnectDeadline) { + clearTimeout(call.timers.reconnectDeadline); + call.timers.reconnectDeadline = null; + } + call.reconnectInProgress = false; + call.reconnectAttempts = 0; +} + +function startReconnectFlow(call, reason = 'disconnected') { + if (!call || call.phase === 'ended') return; + if (!call.connectedAtMs) return; + if (!call.pc) return; + if (call.reconnectInProgress) return; + + call.reconnectInProgress = true; + call.reconnectAttempts = 0; + setStatus(call, 'Связь прервалась. Переподключаем…', 'reconnecting'); + void emitDebug(call, 'warn', 'peer_connection_reconnect_start', `reason=${reason}`); + + const maxAttempts = 6; + const attemptDelayMs = 2500; + const totalDeadlineMs = 17000; + + call.timers.reconnectDeadline = setTimeout(() => { + if (!calls.has(call.callId) || call.phase === 'ended') return; + if (call.pc?.connectionState === 'connected') return; + stopReconnectFlow(call); + void finalizeCall(call, { localReasonCode: 'error', debugReason: 'reconnect_timeout' }); + }, totalDeadlineMs); + + const runAttempt = async () => { + if (!calls.has(call.callId) || call.phase === 'ended') return; + if (!call.pc || call.pc.connectionState === 'connected') { + stopReconnectFlow(call); + return; + } + + call.reconnectAttempts += 1; + try { + const offer = await call.pc.createOffer({ iceRestart: true }); + await call.pc.setLocalDescription(offer); + await sendSignal(call, TYPES.OFFER, JSON.stringify(offer)); + await emitDebug(call, 'info', 'peer_connection_reconnect_offer_sent', `attempt=${call.reconnectAttempts}`); + } catch (error) { + await emitDebug(call, 'warn', 'peer_connection_reconnect_offer_failed', `attempt=${call.reconnectAttempts}; error=${toErrorText(error)}`); + } + + if (call.pc?.connectionState === 'connected') { + stopReconnectFlow(call); + return; + } + if (call.reconnectAttempts >= maxAttempts) { + stopReconnectFlow(call); + await finalizeCall(call, { localReasonCode: 'error', debugReason: 'reconnect_attempts_exhausted' }); + return; + } + call.timers.reconnectStep = setTimeout(() => { + void runAttempt(); + }, attemptDelayMs); + }; + + void runAttempt(); +} + function pushCallSummary(call, summaryCode) { if (!call?.peerLogin) return; const outgoing = call.direction === 'out'; @@ -227,6 +297,7 @@ async function finalizeCall(call, { } = {}) { if (!call) return; cleanupTimers(call); + stopReconnectFlow(call); stopTone(); if (notifyRemoteHangup && call.remoteSessionId) { @@ -331,6 +402,7 @@ async function ensurePeerConnection(call) { pc.onconnectionstatechange = () => { const state = pc.connectionState; if (state === 'connected') { + stopReconnectFlow(call); if (!call.connectedAtMs) { call.connectedAtMs = nowMs(); } @@ -339,15 +411,29 @@ async function ensurePeerConnection(call) { return; } if (state === 'failed') { + if (call.connectedAtMs) { + startReconnectFlow(call, 'failed'); + return; + } void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`); - void finalizeCall(call, { localReasonCode: call.connectedAtMs ? 'completed' : 'error', debugReason: 'failed' }); + void finalizeCall(call, { localReasonCode: 'error', debugReason: 'failed' }); return; } - if ((state === 'closed' || state === 'disconnected') && call.phase !== 'ended') { + if (state === 'disconnected' && call.phase !== 'ended') { + if (call.connectedAtMs) { + startReconnectFlow(call, 'disconnected'); + return; + } + void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`); + return; + } + if (state === 'closed' && call.phase !== 'ended') { void emitDebug(call, 'warn', 'peer_connection_closed', `state=${state}`); if (call.connectedAtMs) { - void finalizeCall(call, { localReasonCode: 'completed', debugReason: state }); + void finalizeCall(call, { localReasonCode: 'error', debugReason: state }); + return; } + void finalizeCall(call, { localReasonCode: 'error', debugReason: state }); } }; @@ -531,6 +617,8 @@ export async function startOutgoingCall(peerLogin) { pc: null, localStream: null, muted: false, + reconnectInProgress: false, + reconnectAttempts: 0, debugMode: false, debugRunId: '', debugRole: '', @@ -596,6 +684,8 @@ export async function handleIncomingCallInvite(evt) { pc: null, localStream: null, muted: false, + reconnectInProgress: false, + reconnectAttempts: 0, debugMode: false, debugRunId: '', debugRole: '', diff --git a/shine-UI/js/services/call-ui-service.js b/shine-UI/js/services/call-ui-service.js index e0b9370..e4e0fd6 100644 --- a/shine-UI/js/services/call-ui-service.js +++ b/shine-UI/js/services/call-ui-service.js @@ -54,8 +54,8 @@ function ensureUi() { declineBtn = document.createElement('button'); declineBtn.type = 'button'; - declineBtn.className = 'ghost-btn'; - declineBtn.textContent = 'Отклонить'; + declineBtn.className = 'destructive-btn'; + declineBtn.textContent = 'Сбросить'; declineBtn.addEventListener('click', async () => { await declineIncomingCall(); }); @@ -63,7 +63,7 @@ function ensureUi() { hangupBtn = document.createElement('button'); hangupBtn.type = 'button'; hangupBtn.className = 'destructive-btn'; - hangupBtn.textContent = 'Положить'; + hangupBtn.textContent = 'Положить трубку'; hangupBtn.addEventListener('click', async () => { await hangupActiveCall(); });