SHA256
CallDeliveryReport: универсальный формат type/value и расширенные отчёты по звонкам
This commit is contained in:
@@ -93,12 +93,25 @@ async function resolveIceServers(call) {
|
||||
const turnUrls = uniqueUrls(parseIceUrls(payload?.turnUrls));
|
||||
const turnUsername = String(payload?.turnUsername || '').trim();
|
||||
const turnPassword = String(payload?.turnPassword || '').trim();
|
||||
const turnServers = Array.isArray(payload?.turnServers) ? payload.turnServers : [];
|
||||
|
||||
const iceServers = [];
|
||||
if (stunUrls.length > 0) {
|
||||
iceServers.push({ urls: stunUrls.length === 1 ? stunUrls[0] : stunUrls });
|
||||
}
|
||||
if (turnUrls.length > 0 && turnUsername && turnPassword) {
|
||||
if (turnServers.length > 0) {
|
||||
turnServers.forEach((item) => {
|
||||
const urls = uniqueUrls(parseIceUrls(item?.urls));
|
||||
const username = String(item?.username || '').trim();
|
||||
const password = String(item?.password || '').trim();
|
||||
if (urls.length === 0 || !username || !password) return;
|
||||
iceServers.push({
|
||||
urls: urls.length === 1 ? urls[0] : urls,
|
||||
username,
|
||||
credential: password,
|
||||
});
|
||||
});
|
||||
} else if (turnUrls.length > 0 && turnUsername && turnPassword) {
|
||||
iceServers.push({
|
||||
urls: turnUrls.length === 1 ? turnUrls[0] : turnUrls,
|
||||
username: turnUsername,
|
||||
@@ -110,7 +123,7 @@ async function resolveIceServers(call) {
|
||||
await emitDebug(call, 'warn', 'call_ice_empty_from_server', 'using_default_stun');
|
||||
return cloneDefaultIceServers();
|
||||
}
|
||||
await emitDebug(call, 'info', 'call_ice_loaded_from_server', `stun=${stunUrls.length}; turn=${turnUrls.length}`);
|
||||
await emitDebug(call, 'info', 'call_ice_loaded_from_server', `stun=${stunUrls.length}; turnEntries=${Math.max(0, iceServers.length - (stunUrls.length > 0 ? 1 : 0))}`);
|
||||
return iceServers;
|
||||
} catch (error) {
|
||||
await emitDebug(call, 'warn', 'call_ice_load_failed', toErrorText(error));
|
||||
@@ -236,6 +249,59 @@ function setActiveStatus(call) {
|
||||
setStatus(call, buildActiveStatusText(call), 'active');
|
||||
}
|
||||
|
||||
function toIsoTs(ts) {
|
||||
const n = Number(ts || 0);
|
||||
if (!Number.isFinite(n) || n <= 0) return '';
|
||||
try { return new Date(n).toISOString(); } catch { return ''; }
|
||||
}
|
||||
|
||||
function buildCallFactsJson(call, extra = {}) {
|
||||
const pc = call?.pc || null;
|
||||
const facts = {
|
||||
callId: call?.callId || '',
|
||||
peerLogin: call?.peerLogin || '',
|
||||
remoteSessionId: call?.remoteSessionId || '',
|
||||
direction: call?.direction || '',
|
||||
phase: call?.phase || '',
|
||||
statusText: call?.statusText || '',
|
||||
startedAtMs: Number(call?.startedAtMs || 0),
|
||||
startedAtIso: toIsoTs(call?.startedAtMs),
|
||||
connectedAtMs: Number(call?.connectedAtMs || 0),
|
||||
connectedAtIso: toIsoTs(call?.connectedAtMs),
|
||||
routeLabel: call?.connectionRouteLabel || '',
|
||||
routeDetails: call?.connectionRouteDetails || '',
|
||||
pcConnectionState: pc?.connectionState || '',
|
||||
pcIceConnectionState: pc?.iceConnectionState || '',
|
||||
pcSignalingState: pc?.signalingState || '',
|
||||
hasLocalStream: Boolean(call?.localStream),
|
||||
localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0,
|
||||
...extra,
|
||||
};
|
||||
try {
|
||||
return JSON.stringify(facts);
|
||||
} catch {
|
||||
return JSON.stringify({ callId: call?.callId || '', serializeError: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCallDeliveryReport(call, eventType, eventCode, reason = '', extraFacts = {}) {
|
||||
if (!call || !authService || typeof authService.sendCallDeliveryReport !== 'function') return;
|
||||
try {
|
||||
const valueJson = buildCallFactsJson(call, {
|
||||
eventType: String(eventType || '').trim(),
|
||||
eventCode: String(eventCode || '').trim(),
|
||||
reason: String(reason || '').trim(),
|
||||
reportedAtMs: nowMs(),
|
||||
reportedAtIso: toIsoTs(nowMs()),
|
||||
...extraFacts,
|
||||
});
|
||||
await authService.sendCallDeliveryReport({
|
||||
type: String(eventType || '').trim(),
|
||||
value: valueJson,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function cleanupTimers(call) {
|
||||
if (call.timers?.ack10s) clearTimeout(call.timers.ack10s);
|
||||
if (call.timers?.total35s) clearTimeout(call.timers.total35s);
|
||||
@@ -251,6 +317,7 @@ async function closeMedia(call) {
|
||||
call.localStream = null;
|
||||
call.audioSenders = [];
|
||||
call.connectionRouteLabel = '';
|
||||
call.connectionRouteDetails = '';
|
||||
}
|
||||
|
||||
function stopReconnectFlow(call) {
|
||||
@@ -269,7 +336,9 @@ function stopReconnectFlow(call) {
|
||||
|
||||
async function detectConnectionRoute(call) {
|
||||
const pc = call?.pc;
|
||||
if (!pc || typeof pc.getStats !== 'function') return { label: '', details: '' };
|
||||
if (!pc || typeof pc.getStats !== 'function') {
|
||||
return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
|
||||
}
|
||||
try {
|
||||
const stats = await pc.getStats();
|
||||
let selectedPair = null;
|
||||
@@ -298,7 +367,7 @@ async function detectConnectionRoute(call) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedPair) return { label: '', details: '' };
|
||||
if (!selectedPair) return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
|
||||
|
||||
const local = selectedPair.localCandidateId && typeof stats.get === 'function'
|
||||
? stats.get(selectedPair.localCandidateId)
|
||||
@@ -309,17 +378,39 @@ async function detectConnectionRoute(call) {
|
||||
|
||||
const localType = String(local?.candidateType || '').trim().toLowerCase();
|
||||
const remoteType = String(remote?.candidateType || '').trim().toLowerCase();
|
||||
const details = `local=${localType || '-'}; remote=${remoteType || '-'}`;
|
||||
const localIp = String(local?.ip || local?.address || '');
|
||||
const remoteIp = String(remote?.ip || remote?.address || '');
|
||||
const localPort = String(local?.port || '');
|
||||
const remotePort = String(remote?.port || '');
|
||||
const relayProto = String(local?.relayProtocol || remote?.relayProtocol || '');
|
||||
const turnCandidateAddress = localType === 'relay'
|
||||
? `${localIp}${localPort ? `:${localPort}` : ''}`
|
||||
: (remoteType === 'relay' ? `${remoteIp}${remotePort ? `:${remotePort}` : ''}` : '');
|
||||
const details = `local=${localType || '-'}(${localIp || '-'}${localPort ? `:${localPort}` : ''}); remote=${remoteType || '-'}(${remoteIp || '-'}${remotePort ? `:${remotePort}` : ''})`;
|
||||
|
||||
if (localType === 'relay' || remoteType === 'relay') {
|
||||
return { label: 'через TURN', details };
|
||||
const label = turnCandidateAddress
|
||||
? `через TURN (${turnCandidateAddress})`
|
||||
: (relayProto ? `через TURN (${relayProto})` : 'через TURN');
|
||||
return { label, details, localIp, remoteIp, turnCandidateAddress };
|
||||
}
|
||||
if (localType || remoteType) {
|
||||
return { label: 'прямое', details };
|
||||
const sameLan = localIp && remoteIp && (
|
||||
(localIp.startsWith('10.') && remoteIp.startsWith('10.'))
|
||||
|| (localIp.startsWith('192.168.') && remoteIp.startsWith('192.168.'))
|
||||
|| (localIp.startsWith('172.16.') && remoteIp.startsWith('172.16.'))
|
||||
);
|
||||
return {
|
||||
label: sameLan ? 'напрямую в локальной сети' : 'напрямую через интернет',
|
||||
details,
|
||||
localIp,
|
||||
remoteIp,
|
||||
turnCandidateAddress: '',
|
||||
};
|
||||
}
|
||||
return { label: '', details };
|
||||
return { label: '', details, localIp, remoteIp, turnCandidateAddress: '' };
|
||||
} catch {
|
||||
return { label: '', details: '' };
|
||||
return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +427,7 @@ function startTransportProbe(call) {
|
||||
const route = await detectConnectionRoute(call);
|
||||
if (!route.label || route.label === call.connectionRouteLabel) return;
|
||||
call.connectionRouteLabel = route.label;
|
||||
call.connectionRouteDetails = route.details || '';
|
||||
setActiveStatus(call);
|
||||
await emitDebug(call, 'info', 'peer_connection_route', route.details || route.label);
|
||||
};
|
||||
@@ -475,6 +567,18 @@ async function finalizeCall(call, {
|
||||
await emitDebug(call, 'info', 'call_finalize', `${localReasonCode}:${debugReason}`);
|
||||
}
|
||||
|
||||
const reasonText = debugReason || localReasonCode;
|
||||
if (String(localReasonCode || '') !== 'completed') {
|
||||
if (call.direction === 'out') {
|
||||
await sendCallDeliveryReport(call, 'outgoing_failed', `outgoing_${localReasonCode}`, reasonText);
|
||||
} else if (call.direction === 'in') {
|
||||
await sendCallDeliveryReport(call, 'incoming_failed', `incoming_${localReasonCode}`, reasonText);
|
||||
}
|
||||
if (String(localReasonCode || '') === 'error') {
|
||||
await sendCallDeliveryReport(call, 'unknown_error', 'call_unknown_error', reasonText);
|
||||
}
|
||||
}
|
||||
|
||||
pushCallSummary(call, localReasonCode);
|
||||
|
||||
call.phase = 'ended';
|
||||
@@ -565,6 +669,30 @@ async function ensurePeerConnection(call) {
|
||||
setActiveStatus(call);
|
||||
startTransportProbe(call);
|
||||
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
||||
if (call.direction === 'out' && !call.connectionSuccessReported) {
|
||||
call.connectionSuccessReported = true;
|
||||
void (async () => {
|
||||
const route = await detectConnectionRoute(call);
|
||||
if (route?.label) {
|
||||
call.connectionRouteLabel = route.label;
|
||||
}
|
||||
call.connectionRouteDetails = route?.details || '';
|
||||
await sendCallDeliveryReport(
|
||||
call,
|
||||
'call_connected',
|
||||
'call_connected_success',
|
||||
`connected:${route?.label || 'unknown_route'}`,
|
||||
{
|
||||
reportBy: 'initiator',
|
||||
routeLabel: route?.label || '',
|
||||
routeDetails: route?.details || '',
|
||||
localIp: route?.localIp || '',
|
||||
remoteIp: route?.remoteIp || '',
|
||||
turnCandidateAddress: route?.turnCandidateAddress || '',
|
||||
},
|
||||
);
|
||||
})();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state === 'failed') {
|
||||
@@ -599,6 +727,7 @@ async function ensurePeerConnection(call) {
|
||||
call.localStream = stream;
|
||||
call.audioSenders = [];
|
||||
call.connectionRouteLabel = '';
|
||||
call.connectionRouteDetails = '';
|
||||
stream.getTracks().forEach((track) => {
|
||||
track.enabled = track.kind === 'audio' ? !call.muted : true;
|
||||
const sender = pc.addTrack(track, stream);
|
||||
|
||||
Reference in New Issue
Block a user