Перевести звонки на SendSignal с client key

This commit is contained in:
AidarKC
2026-08-09 20:09:54 +04:00
parent 5c38f8f0d8
commit c511910b8f
11 changed files with 556 additions and 43 deletions
+5
View File
@@ -10,6 +10,7 @@ import {
handleIncomingCallInvite,
handleIncomingCallPush,
handleIncomingCallSignal,
handleIncomingCallSignalViaSendSignal,
handleStopCallPush,
setCallDebugReporter,
startDebugConnectionAsInitiator,
@@ -1313,6 +1314,10 @@ async function init() {
try { await handleIncomingCallSignal(evt); } catch {}
});
authService.onEvent('IncomingSignal', async (evt) => {
try { await handleIncomingCallSignalViaSendSignal(evt); } catch {}
});
authService.onEvent('DebugConnectPrepareResponder', async (evt) => {
try {
const p = evt?.payload || {};
+2 -2
View File
@@ -2631,8 +2631,8 @@ export class AuthService {
}
async callInviteBroadcast({ toLogin, callId, type = 100 }) {
const response = await this.ws.request('CallInviteBroadcast', { toLogin, callId, type });
async callInviteBroadcast({ toLogin, callId, type = 100, data = '' }) {
const response = await this.ws.request('CallInviteBroadcast', { toLogin, callId, type, data });
if (response.status !== 200) throw opError('CallInviteBroadcast', response);
return response.payload || {};
}
+182 -32
View File
@@ -8,10 +8,25 @@ const TYPES = {
DECLINE_BUSY: 130,
TIMEOUT: 140,
HANGUP: 150,
CONNECT_START: 170,
OFFER: 200,
ANSWER: 210,
ICE: 220,
};
const SIGNAL_TARGET_SINGLE = 'single_session';
const SIGNAL_TARGET_ALL = 'all_sessions';
const CALL_SIGNAL_TYPES = Object.freeze({
[TYPES.INVITE]: 'call_invite',
[TYPES.RINGING]: 'call_ringing',
[TYPES.ACCEPT]: 'call_accept',
[TYPES.DECLINE_BUSY]: 'call_decline_busy',
[TYPES.TIMEOUT]: 'call_timeout',
[TYPES.HANGUP]: 'call_hangup',
[TYPES.CONNECT_START]: 'call_connect_start',
[TYPES.OFFER]: 'call_offer',
[TYPES.ANSWER]: 'call_answer',
[TYPES.ICE]: 'call_ice',
});
const calls = new Map();
const callStateListeners = new Set();
@@ -71,12 +86,54 @@ function getSignalTypeName(type) {
if (normalized === TYPES.DECLINE_BUSY) return 'decline_busy';
if (normalized === TYPES.TIMEOUT) return 'timeout';
if (normalized === TYPES.HANGUP) return 'hangup';
if (normalized === TYPES.CONNECT_START) return 'connect_start';
if (normalized === TYPES.OFFER) return 'offer';
if (normalized === TYPES.ANSWER) return 'answer';
if (normalized === TYPES.ICE) return 'ice';
return `type_${String(type || '')}`;
}
function buildConnectStartData(winnerSessionId) {
return `session=${String(winnerSessionId || '').trim()}`;
}
function parseConnectStartWinnerSessionId(data = '') {
const raw = String(data || '').trim();
if (!raw) return '';
if (raw.startsWith('session=')) return raw.slice('session='.length).trim();
if (raw.startsWith('winnerSessionId=')) return raw.slice('winnerSessionId='.length).trim();
return '';
}
function getCallSignalType(type) {
return CALL_SIGNAL_TYPES[Number(type)] || '';
}
function encodeCallSignalData(callId, type, data = '') {
return JSON.stringify({
callId: String(callId || '').trim(),
type: Number(type),
data: String(data || ''),
});
}
function decodeCallSignalData(raw) {
try {
const parsed = JSON.parse(String(raw || '{}'));
return {
callId: String(parsed?.callId || '').trim(),
type: Number(parsed?.type),
data: String(parsed?.data || ''),
};
} catch {
return {
callId: '',
type: Number.NaN,
data: '',
};
}
}
function ensureCallTimeline(call) {
if (!call) return;
if (!Array.isArray(call.timelineEvents)) call.timelineEvents = [];
@@ -1168,13 +1225,7 @@ async function finalizeCall(call, {
const dataValue = notifyRemoteHangup
? ''
: `setup_failed:${String(localReasonCode || 'error')}:${String(debugReason || '').slice(0, 80)}`;
await authService.callSignalToSession({
toLogin: call.peerLogin,
targetSessionId: call.remoteSessionId,
callId: call.callId,
type: TYPES.HANGUP,
data: dataValue,
});
await sendSignal(call, TYPES.HANGUP, dataValue);
} catch {}
}
@@ -1255,13 +1306,22 @@ async function emitDebug(call, level, message, details = '') {
async function sendSignal(call, type, data = '') {
if (!call.remoteSessionId) return;
const signalName = getSignalTypeName(type);
const signalType = getCallSignalType(type);
const login = String(state?.session?.login || '').trim();
const storagePwd = String(state?.session?.storagePwdInMemory || '').trim();
if (!login || !storagePwd || !signalType) {
throw new Error('Call SendSignal: missing login/storagePwd/signalType');
}
try {
await authService.callSignalToSession({
await authService.sendSignal({
toLogin: call.peerLogin,
targetMode: SIGNAL_TARGET_SINGLE,
targetSessionId: call.remoteSessionId,
callId: call.callId,
type,
data,
signalType,
signalRequestId: call.callId,
data: encodeCallSignalData(call.callId, type, data),
storagePwd,
includeClientSignature: true,
});
recordCallTimeline(call, `signal_out_${signalName}`, `toSession=${call.remoteSessionId}; len=${String(data || '').length}`);
await emitDebug(call, 'info', `signal_sent_${type}`, `len=${String(data || '').length}`);
@@ -1280,6 +1340,50 @@ async function sendSignal(call, type, data = '') {
}
}
async function broadcastSignal(call, type, data = '') {
const signalName = getSignalTypeName(type);
const signalType = getCallSignalType(type);
const login = String(state?.session?.login || '').trim();
const storagePwd = String(state?.session?.storagePwdInMemory || '').trim();
if (!call?.peerLogin || !login || !storagePwd || !signalType) {
throw new Error('Call SendSignal broadcast: missing peer/login/storagePwd/signalType');
}
try {
const response = await authService.sendSignal({
toLogin: call.peerLogin,
targetMode: SIGNAL_TARGET_ALL,
signalType,
signalRequestId: call.callId,
data: encodeCallSignalData(call.callId, type, data),
storagePwd,
includeClientSignature: true,
});
recordCallTimeline(
call,
`signal_broadcast_${signalName}`,
`ws=${Number(response?.deliveredWsSessions || response?.deliveredCount || 0)}; push=${Number(response?.deliveredWebPushSessions || response?.deliveredFcmSessions || 0)}`,
);
return response || {};
} catch (error) {
recordCallTimeline(call, `signal_broadcast_${signalName}_failed`, toErrorText(error), 'warn');
throw error;
}
}
async function notifyPeerSessionsAboutConnectStart(call) {
const winnerSessionId = String(call?.remoteSessionId || '').trim();
if (!call || !winnerSessionId) return;
const data = buildConnectStartData(winnerSessionId);
try {
await broadcastSignal(call, TYPES.CONNECT_START, data);
recordCallTimeline(call, 'connect_start_broadcast_sent', `winnerSession=${winnerSessionId}`);
await emitDebug(call, 'info', 'connect_start_broadcast_sent', `winnerSession=${winnerSessionId}`);
} catch (error) {
recordCallTimeline(call, 'connect_start_broadcast_failed', `winnerSession=${winnerSessionId}; error=${toErrorText(error)}`, 'warn');
await emitDebug(call, 'warn', 'connect_start_broadcast_failed', toErrorText(error));
}
}
async function ensurePeerConnection(call) {
if (call.pc) return call.pc;
@@ -1451,6 +1555,7 @@ async function onAccept(call) {
cleanupTimers(call);
setStatus(call, 'Соединяем…', 'connecting');
try {
await notifyPeerSessionsAboutConnectStart(call);
const pc = await ensurePeerConnection(call);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
@@ -1496,16 +1601,40 @@ async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) {
const callId = String(payload?.callId || '').trim();
const fromLogin = String(payload?.fromLogin || '').trim();
const fromSessionId = String(payload?.fromSessionId || '').trim();
const type = Number(payload?.type || TYPES.INVITE);
if (type === TYPES.CONNECT_START) {
const call = getCall(callId);
if (!call || call.direction !== 'in' || call.phase === 'ended') return call;
const winnerSessionId = parseConnectStartWinnerSessionId(payload?.data || '');
const currentSessionId = String(state?.session?.sessionId || '').trim();
recordCallTimeline(call, 'connect_start_broadcast_received', `winnerSession=${winnerSessionId || '-'}; currentSession=${currentSessionId || '-'}`);
if (winnerSessionId && currentSessionId && winnerSessionId === currentSessionId) {
await emitDebug(call, 'info', 'connect_start_kept_on_winner_session', winnerSessionId);
return call;
}
dismissCallUiLocally(call);
await finalizeCall(call, {
localReasonCode: 'completed',
debugReason: 'answered_elsewhere_session_selected',
suppressRemoteSignal: true,
suppressReports: true,
suppressSummary: true,
});
return call;
}
if (!callId || !fromLogin || !fromSessionId) return null;
if (activeCallId && activeCallId !== callId) {
try {
await authService.callSignalToSession({
await authService.sendSignal({
toLogin: fromLogin,
targetMode: SIGNAL_TARGET_SINGLE,
targetSessionId: fromSessionId,
callId,
type: TYPES.DECLINE_BUSY,
data: 'busy',
signalType: getCallSignalType(TYPES.DECLINE_BUSY),
signalRequestId: callId,
data: encodeCallSignalData(callId, TYPES.DECLINE_BUSY, 'busy'),
storagePwd: state?.session?.storagePwdInMemory || '',
includeClientSignature: true,
});
} catch {}
return null;
@@ -1804,7 +1933,7 @@ export async function startOutgoingCall(peerLogin) {
}, 35000);
try {
call.inviteDelivery = await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
call.inviteDelivery = await broadcastSignal(call, TYPES.INVITE);
recordCallTimeline(
call,
'invite_broadcast_ok',
@@ -1822,7 +1951,7 @@ export async function startOutgoingCall(peerLogin) {
const recovered = await ensureSessionForCall({ timeoutMs: preflightTimeoutMs, force: true });
if (recovered) {
try {
call.inviteDelivery = await authService.callInviteBroadcast({ toLogin: cleanPeer, callId, type: TYPES.INVITE });
call.inviteDelivery = await broadcastSignal(call, TYPES.INVITE);
if (getInviteDeliveredCount(call) > 0 && call.phase === 'searching') {
setStatus(call, 'Вызываем…', 'ringing');
}
@@ -1842,6 +1971,33 @@ export async function handleIncomingCallInvite(evt) {
await handleIncomingInvitePayload(evt?.payload || {}, { source: 'ws' });
}
export async function handleIncomingCallSignalViaSendSignal(evt) {
const payload = evt?.payload || {};
const signalType = String(payload?.signalType || '').trim();
if (!signalType.startsWith('call_')) return false;
const decoded = decodeCallSignalData(payload?.data || '');
if (!Number.isFinite(decoded.type)) return true;
const normalizedEvent = {
payload: {
callId: decoded.callId || String(payload?.signalRequestId || '').trim(),
fromLogin: payload?.fromLogin,
fromSessionId: payload?.fromSessionId,
type: decoded.type,
data: decoded.data,
},
};
if (decoded.type === TYPES.INVITE || decoded.type === TYPES.CONNECT_START) {
await handleIncomingInvitePayload(normalizedEvent.payload, { source: 'send_signal' });
return true;
}
await handleIncomingCallSignal(normalizedEvent);
return true;
}
export async function acceptIncomingCall() {
const call = getActiveCall();
if (!call || call.direction !== 'in' || call.phase !== 'incoming') return;
@@ -1885,23 +2041,11 @@ export async function handleIncomingCallSignal(evt) {
recordCallTimeline(call, `signal_in_${signalName}`, `from=${fromSessionId || '-'}; len=${data.length}`);
if (call.direction === 'out') {
if (type === TYPES.RINGING) {
if (!call.remoteSessionId && fromSessionId) {
setRemoteSessionId(call, fromSessionId, 'ringing');
}
if (call.remoteSessionId && fromSessionId && call.remoteSessionId !== fromSessionId) {
recordCallTimeline(call, 'signal_in_ringing_ignored', `selected=${call.remoteSessionId}; from=${fromSessionId}`);
await emitDebug(
call,
'info',
'ringing_from_non_selected_session_ignored',
`selected=${call.remoteSessionId}; from=${fromSessionId}`,
);
return;
}
recordCallTimeline(call, 'ringing_received', `from=${fromSessionId || '-'}`);
} else if (type === TYPES.ACCEPT) {
if (fromSessionId) {
if (!call.remoteSessionId || !call.initialOfferSent) {
setRemoteSessionId(call, fromSessionId, 'accept_before_offer_lock');
if (!call.remoteSessionId) {
setRemoteSessionId(call, fromSessionId, 'accept');
} else if (call.remoteSessionId !== fromSessionId) {
recordCallTimeline(call, 'signal_in_accept_ignored', `selected=${call.remoteSessionId}; from=${fromSessionId}`, 'warn');
await emitDebug(
@@ -1961,6 +2105,12 @@ export async function handleIncomingCallSignal(evt) {
return;
}
if (type === TYPES.CONNECT_START) {
const winnerSessionId = parseConnectStartWinnerSessionId(data);
recordCallTimeline(call, 'connect_start_received', `winnerSession=${winnerSessionId || '-'}; currentRemoteSession=${call.remoteSessionId || '-'}`);
return;
}
if (type === TYPES.ACCEPT) {
if (call.direction !== 'out') {
await emitDebug(call, 'warn', 'accept_ignored_for_non_outgoing_call', `direction=${call.direction || ''}`);