fix(call): кнопки входящего и автопереподключение после обрыва

This commit is contained in:
AidarKC 2026-04-22 17:33:57 +03:00
parent 97a2bee81a
commit d7c7bb3c23
2 changed files with 98 additions and 8 deletions

View File

@ -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: '',

View File

@ -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();
});