@@ -1,4 +1,4 @@
import { addChatMessage , authService } from '../state.js' ;
import { addSystemChatMessage , authService } from '../state.js' ;
const TYPES = {
INVITE : 100 ,
@@ -13,9 +13,16 @@ const TYPES = {
} ;
const calls = new Map ( ) ;
const callStateListeners = new Set ( ) ;
let activeCallId = '' ;
let debugReporter = null ;
let audioContext = null ;
let toneTimerId = null ;
let toneName = '' ;
let toneFlip = false ;
function nowMs ( ) {
return Date . now ( ) ;
}
@@ -28,10 +35,233 @@ function getCall(callId) {
return calls . get ( callId ) || null ;
}
function getActiveCall ( ) {
if ( ! activeCallId ) return null ;
return getCall ( activeCallId ) ;
}
function toErrorText ( error ) {
return error ? . message || String ( error || 'unknown' ) ;
}
function formatDuration ( ms ) {
const totalSec = Math . max ( 0 , Math . round ( Number ( ms || 0 ) / 1000 ) ) ;
const sec = totalSec % 60 ;
const totalMin = Math . floor ( totalSec / 60 ) ;
const min = totalMin % 60 ;
const hours = Math . floor ( totalMin / 60 ) ;
if ( hours > 0 ) return ` ${ hours } ч ${ min } м ${ sec } с ` ;
if ( min > 0 ) return ` ${ min } м ${ sec } с ` ;
return ` ${ sec } с ` ;
}
function ensureAudioContext ( ) {
if ( audioContext ) return audioContext ;
const Ctx = window . AudioContext || window . webkitAudioContext ;
if ( ! Ctx ) return null ;
audioContext = new Ctx ( ) ;
return audioContext ;
}
function playBeep ( freq = 440 , durationMs = 120 , gainValue = 0.08 ) {
const ctx = ensureAudioContext ( ) ;
if ( ! ctx ) return ;
if ( ctx . state === 'suspended' ) {
void ctx . resume ( ) . catch ( ( ) => { } ) ;
}
const osc = ctx . createOscillator ( ) ;
const gain = ctx . createGain ( ) ;
osc . type = 'sine' ;
osc . frequency . value = Number ( freq ) || 440 ;
gain . gain . value = gainValue ;
osc . connect ( gain ) ;
gain . connect ( ctx . destination ) ;
const now = ctx . currentTime ;
osc . start ( now ) ;
osc . stop ( now + Math . max ( 0.03 , durationMs / 1000 ) ) ;
}
function stopTone ( ) {
if ( toneTimerId ) {
clearInterval ( toneTimerId ) ;
toneTimerId = null ;
}
toneName = '' ;
toneFlip = false ;
}
function startTone ( nextToneName ) {
if ( ! nextToneName ) {
stopTone ( ) ;
return ;
}
if ( toneName === nextToneName ) return ;
stopTone ( ) ;
toneName = nextToneName ;
if ( nextToneName === 'searching' ) {
toneTimerId = window . setInterval ( ( ) => {
toneFlip = ! toneFlip ;
playBeep ( toneFlip ? 920 : 760 , 120 , 0.07 ) ;
} , 420 ) ;
return ;
}
if ( nextToneName === 'ringback' ) {
playBeep ( 425 , 900 , 0.08 ) ;
toneTimerId = window . setInterval ( ( ) => {
playBeep ( 425 , 900 , 0.08 ) ;
} , 4000 ) ;
return ;
}
if ( nextToneName === 'incoming' ) {
const hit = ( ) => {
playBeep ( 830 , 180 , 0.09 ) ;
window . setTimeout ( ( ) => playBeep ( 680 , 180 , 0.09 ) , 240 ) ;
} ;
hit ( ) ;
toneTimerId = window . setInterval ( hit , 2000 ) ;
}
}
function getCallStateSnapshot ( ) {
const call = getActiveCall ( ) ;
if ( ! call ) return null ;
const callPhase = String ( call . phase || '' ) . trim ( ) ;
return {
callId : call . callId ,
peerLogin : call . peerLogin || '' ,
direction : call . direction || 'out' ,
phase : callPhase ,
statusText : call . statusText || '' ,
muted : Boolean ( call . muted ) ,
canAnswer : callPhase === 'incoming' ,
canDecline : callPhase === 'incoming' ,
canHangup : callPhase !== 'ended' ,
canMute : callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing' ,
} ;
}
function notifyCallState ( ) {
const snapshot = getCallStateSnapshot ( ) ;
callStateListeners . forEach ( ( listener ) => {
try {
listener ( snapshot ) ;
} catch { }
} ) ;
}
function setStatus ( call , statusText , phase = '' ) {
if ( ! call ) return ;
call . statusText = String ( statusText || '' ) . trim ( ) ;
if ( phase ) {
call . phase = String ( phase || '' ) . trim ( ) ;
}
if ( call . phase === 'searching' ) startTone ( 'searching' ) ;
else if ( call . phase === 'ringing' ) startTone ( 'ringback' ) ;
else if ( call . phase === 'incoming' ) startTone ( 'incoming' ) ;
else stopTone ( ) ;
void emitDebug ( call , 'info' , ` call_status: ${ call . statusText } ` , ` callId= ${ call . callId } ` ) ;
notifyCallState ( ) ;
}
function cleanupTimers ( call ) {
if ( call . timers ? . ack10s ) clearTimeout ( call . timers . ack10s ) ;
if ( call . timers ? . total35s ) clearTimeout ( call . timers . total35s ) ;
if ( call . timers ? . incoming20s ) clearTimeout ( call . timers . incoming20s ) ;
}
async function closeMedia ( call ) {
try { call . pc ? . close ( ) ; } catch { }
try { call . localStream ? . getTracks ( ) ? . forEach ( ( track ) => track . stop ( ) ) ; } catch { }
call . pc = null ;
call . localStream = null ;
}
function pushCallSummary ( call , summaryCode ) {
if ( ! call ? . peerLogin ) return ;
const outgoing = call . direction === 'out' ;
const from = outgoing ? 'out' : 'in' ;
if ( summaryCode === 'busy' ) {
const text = outgoing
? '[Звонок] Исходящий: не дозвонились (занято)'
: ` [Звонок] Пропущенный входящий от ${ call . peerLogin } (занято) ` ;
addSystemChatMessage ( call . peerLogin , text , { from , kind : 'call-tech' } ) ;
return ;
}
if ( summaryCode === 'no_answer' ) {
const text = outgoing
? '[Звонок] Исходящий: не дозвонились (нет ответа)'
: ` [Звонок] Пропущенный входящий от ${ call . peerLogin } ` ;
addSystemChatMessage ( call . peerLogin , text , { from , kind : 'call-tech' } ) ;
return ;
}
if ( summaryCode === 'declined' ) {
const text = outgoing
? '[Звонок] Исходящий: не дозвонились (отклонён)'
: ` [Звонок] Входящий звонок от ${ call . peerLogin } отклонён ` ;
addSystemChatMessage ( call . peerLogin , text , { from , kind : 'call-tech' } ) ;
return ;
}
if ( summaryCode === 'error' ) {
const text = outgoing
? '[Звонок] Исходящий: не дозвонились (ошибка соединения)'
: ` [Звонок] Входящий звонок от ${ call . peerLogin } : ошибка соединения ` ;
addSystemChatMessage ( call . peerLogin , text , { from , kind : 'call-tech' } ) ;
return ;
}
if ( summaryCode === 'completed' ) {
const duration = formatDuration ( nowMs ( ) - Number ( call . connectedAtMs || call . startedAtMs || nowMs ( ) ) ) ;
const label = outgoing ? 'Исходящий' : 'Входящий' ;
addSystemChatMessage ( call . peerLogin , ` [Звонок] ${ label } : разговор ${ duration } ` , {
from ,
kind : 'call-tech' ,
} ) ;
}
}
async function finalizeCall ( call , {
localReasonCode = 'error' ,
debugReason = '' ,
notifyRemoteHangup = false ,
} = { } ) {
if ( ! call ) return ;
cleanupTimers ( call ) ;
stopTone ( ) ;
if ( notifyRemoteHangup && call . remoteSessionId ) {
try {
await authService . callSignalToSession ( {
toLogin : call . peerLogin ,
targetSessionId : call . remoteSessionId ,
callId : call . callId ,
type : TYPES . HANGUP ,
data : '' ,
} ) ;
} catch { }
}
await closeMedia ( call ) ;
if ( String ( localReasonCode || '' ) === 'completed' ) {
await emitDebug ( call , 'info' , 'debug_connection_success' , debugReason || 'completed' ) ;
}
if ( debugReason ) {
await emitDebug ( call , 'info' , 'call_finalize' , ` ${ localReasonCode } : ${ debugReason } ` ) ;
}
pushCallSummary ( call , localReasonCode ) ;
call . phase = 'ended' ;
call . statusText = 'Звонок завершён' ;
notifyCallState ( ) ;
calls . delete ( call . callId ) ;
if ( activeCallId === call . callId ) {
activeCallId = '' ;
}
notifyCallState ( ) ;
}
async function emitDebug ( call , level , message , details = '' ) {
if ( ! call ? . debugRunId || typeof debugReporter !== 'function' ) return ;
try {
@@ -44,48 +274,6 @@ async function emitDebug(call, level, message, details = '') {
} catch { }
}
function setStatus ( call , text ) {
call . status = text ;
addChatMessage ( call . peerLogin || 'debug-peer' , ` [call] ${ text } ` ) ;
void emitDebug ( call , 'info' , ` call_status: ${ text } ` , ` callId= ${ call . callId } ` ) ;
}
function cleanupTimers ( call ) {
if ( call . timers ? . ack5s ) clearTimeout ( call . timers . ack5s ) ;
if ( call . timers ? . total22s ) clearTimeout ( call . timers . total22s ) ;
if ( call . timers ? . incoming20s ) clearTimeout ( call . timers . incoming20s ) ;
}
async function closeMedia ( call ) {
try { call . pc ? . close ( ) ; } catch { }
try { call . localStream ? . getTracks ( ) ? . forEach ( ( t ) => t . stop ( ) ) ; } catch { }
call . pc = null ;
call . localStream = null ;
}
async function finishCall ( call , reason , notifyRemote = false ) {
if ( ! call ) return ;
cleanupTimers ( call ) ;
if ( notifyRemote && call . remoteSessionId ) {
try {
await authService . callSignalToSession ( {
toLogin : call . peerLogin ,
targetSessionId : call . remoteSessionId ,
callId : call . callId ,
type : TYPES . HANGUP ,
data : '' ,
} ) ;
} catch { }
}
await closeMedia ( call ) ;
setStatus ( call , ` завершен: ${ reason } ` ) ;
if ( String ( reason || '' ) . toLowerCase ( ) . includes ( 'connected' ) ) {
await emitDebug ( call , 'info' , 'debug_connection_success' , reason ) ;
}
calls . delete ( call . callId ) ;
if ( activeCallId === call . callId ) activeCallId = '' ;
}
async function sendSignal ( call , type , data = '' ) {
if ( ! call . remoteSessionId ) return ;
try {
@@ -143,22 +331,37 @@ async function ensurePeerConnection(call) {
pc . onconnectionstatechange = ( ) => {
const state = pc . connectionState ;
if ( state === 'connected' ) {
setStatus ( call , 'соединение установлено' ) ;
if ( ! call . connectedAtMs ) {
call . connectedAtMs = nowMs ( ) ;
}
setStatus ( call , 'Разговор идёт' , 'active' ) ;
void emitDebug ( call , 'info' , 'peer_connection_connected' , ` callId= ${ call . callId } ` ) ;
return ;
}
if ( state === 'failed' || state === 'closed' || state === 'disconnected' ) {
if ( state === 'failed' ) {
void emitDebug ( call , 'warn' , 'peer_connection_closed' , ` state= ${ state } ` ) ;
finishCall ( call , ` state= ${ state } ` , false ) ;
void finalizeCall ( call , { localReasonCode : call . connectedAtMs ? 'completed' : 'error' , debugReason : 'failed' } ) ;
return ;
}
if ( ( state === 'closed' || state === 'disconnected' ) && call . phase !== 'ended' ) {
void emitDebug ( call , 'warn' , 'peer_connection_closed' , ` state= ${ state } ` ) ;
if ( call . connectedAtMs ) {
void finalizeCall ( call , { localReasonCode : 'completed' , debugReason : state } ) ;
}
}
} ;
try {
const stream = await navigator . mediaDevices . getUserMedia ( { audio : true , video : false } ) ;
call . localStream = stream ;
stream . getTracks ( ) . forEach ( ( track ) => pc . addTrack ( track , stream ) ) ;
stream . getTracks ( ) . forEach ( ( track ) => {
track . enabled = ! call . muted ;
pc . addTrack ( track , stream ) ;
} ) ;
} catch ( e ) {
setStatus ( call , ` н ет доступа к микрофону: ${ e ? . message || 'unknown' } ` ) ;
setStatus ( call , ` Н ет доступа к микрофону: ${ e ? . message || 'unknown' } ` , 'failed' ) ;
await emitDebug ( call , 'warn' , 'microphone_access_failed' , toErrorText ( e ) ) ;
throw e ;
}
pc . ontrack = ( evt ) => {
@@ -174,17 +377,68 @@ async function ensurePeerConnection(call) {
async function onAccept ( call ) {
cleanupTimers ( call ) ;
setStatus ( call , 'Соединяем…' , 'connecting' ) ;
const pc = await ensurePeerConnection ( call ) ;
const offer = await pc . createOffer ( ) ;
await pc . setLocalDescription ( offer ) ;
await sendSignal ( call , TYPES . OFFER , JSON . stringify ( offer ) ) ;
setStatus ( call , 'отправлен offer ' ) ;
await emitDebug ( call , 'info' , 'offer_sent' , 'offer created and sent ') ;
}
function ensureIncomingNotification ( peerLogin ) {
if ( typeof window === 'undefined' ) return ;
const text = ` Вам звонит ${ peerLogin } ` ;
try {
if ( 'Notification' in window && Notification . permission === 'granted' ) {
new Notification ( 'SHiNE: входящий звонок' , { body : text } ) ;
}
} catch { }
try {
if ( 'vibrate' in navigator ) {
navigator . vibrate ( [ 180 , 70 , 180 , 70 , 260 ] ) ;
}
} catch { }
}
export function setCallDebugReporter ( fn ) {
debugReporter = typeof fn === 'function' ? fn : null ;
}
export function subscribeCallState ( listener ) {
if ( typeof listener !== 'function' ) {
return ( ) => { } ;
}
callStateListeners . add ( listener ) ;
try {
listener ( getCallStateSnapshot ( ) ) ;
} catch { }
return ( ) => {
callStateListeners . delete ( listener ) ;
} ;
}
export function getActiveCallState ( ) {
return getCallStateSnapshot ( ) ;
}
export async function setMicMuted ( muted ) {
const call = getActiveCall ( ) ;
if ( ! call ) return ;
call . muted = Boolean ( muted ) ;
try {
call . localStream ? . getAudioTracks ( ) ? . forEach ( ( track ) => {
track . enabled = ! call . muted ;
} ) ;
} catch { }
notifyCallState ( ) ;
}
export async function toggleMicMuted ( ) {
const call = getActiveCall ( ) ;
if ( ! call ) return ;
await setMicMuted ( ! call . muted ) ;
}
export async function startDebugConnectionAsResponder ( { runId , callId , peerLogin , peerSessionId } ) {
const cleanCallId = String ( callId || '' ) . trim ( ) ;
const cleanPeerLogin = String ( peerLogin || '' ) . trim ( ) ;
@@ -197,12 +451,15 @@ export async function startDebugConnectionAsResponder({ runId, callId, peerLogin
callId : cleanCallId ,
peerLogin : cleanPeerLogin ,
direction : 'in' ,
state : 'accepted ' ,
phase : 'incoming ' ,
statusText : 'Debug: responder ждёт offer' ,
remoteSessionId : cleanPeerSessionId ,
timers : { } ,
startedAtMs : nowMs ( ) ,
connectedAtMs : 0 ,
pc : null ,
localStream : null ,
muted : false ,
debugMode : true ,
debugRunId : String ( runId || '' ) . trim ( ) ,
debugRole : 'responder' ,
@@ -212,7 +469,7 @@ export async function startDebugConnectionAsResponder({ runId, callId, peerLogin
activeCallId = cleanCallId ;
await emitDebug ( call , 'info' , 'debug_prepare_responder' , ` peerSessionId= ${ cleanPeerSessionId } ` ) ;
setStatus ( call , 'd ebug: responder готов, ждём offer' ) ;
setStatus ( call , 'D ebug: responder готов, ждём offer' , 'incoming' ) ;
}
export async function startDebugConnectionAsInitiator ( { runId , callId , peerLogin , peerSessionId } ) {
@@ -225,12 +482,15 @@ export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin
callId : cleanCallId ,
peerLogin : cleanPeerLogin ,
direction : 'out' ,
state : 'accepted ' ,
phase : 'connecting ' ,
statusText : 'Debug: старт соединения' ,
remoteSessionId : cleanPeerSessionId ,
timers : { } ,
startedAtMs : nowMs ( ) ,
connectedAtMs : 0 ,
pc : null ,
localStream : null ,
muted : false ,
debugMode : true ,
debugRunId : String ( runId || '' ) . trim ( ) ,
debugRole : 'initiator' ,
@@ -238,12 +498,13 @@ export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin
calls . set ( cleanCallId , call ) ;
activeCallId = cleanCallId ;
notifyCallState ( ) ;
await emitDebug ( call , 'info' , 'debug_start_initiator' , ` peerSessionId= ${ cleanPeerSessionId } ` ) ;
try {
await onAccept ( call ) ;
} catch ( error ) {
await emitDebug ( call , 'error' , 'debug_initiator_start_failed' , toErrorText ( error ) ) ;
await finishCall ( call , ` debug start failed: ${ toErrorText ( error ) } ` , false ) ;
await finalizeCall ( call , { localReasonCode : 'error' , debugReason : toErrorText ( error ) } ) ;
}
}
@@ -251,9 +512,9 @@ export async function startOutgoingCall(peerLogin) {
const cleanPeer = String ( peerLogin || '' ) . trim ( ) ;
if ( ! cleanPeer ) return ;
if ( activeCallId ) {
addChatMessage ( cleanPeer , '[call] уже есть активный звонок' ) ;
return ;
const active = getActiveCall ( ) ;
if ( active ) {
throw new Error ( ` Уже есть активный звонок с ${ active . peerLogin || 'другим пользователем' } ` ) ;
}
const callId = makeCallId ( ) ;
@@ -261,31 +522,43 @@ export async function startOutgoingCall(peerLogin) {
callId ,
peerLogin : cleanPeer ,
direction : 'out' ,
state : 'dial ing' ,
phase : 'search ing' ,
statusText : 'Ищем пользователя…' ,
remoteSessionId : '' ,
timers : { } ,
startedAtMs : nowMs ( ) ,
connectedAtMs : 0 ,
pc : null ,
localStream : null ,
muted : false ,
debugMode : false ,
debugRunId : '' ,
debugRole : '' ,
} ;
calls . set ( callId , call ) ;
activeCallId = callId ;
setStatus ( call , 'набираем... ' ) ;
setStatus ( call , 'Ищем пользователя…' , 'searching ') ;
call . timers . ack5s = setTimeout ( ( ) => {
if ( call . state === 'dialing' ) {
finishCall ( call , 'нет ответа 5с ', false ) ;
call . timers . ack10s = setTimeout ( ( ) => {
if ( ! calls . has ( callId ) ) return ;
if ( call . phase === 'searching ') {
void finalizeCall ( call , { localReasonCode : 'no_answer' , debugReason : 'no_ack_10s' } ) ;
}
} , 5000 ) ;
} , 10000 ) ;
call . timers . total22s = setTimeout ( ( ) => {
finishCall ( call , 'таймаут 22с ' , false ) ;
} , 22000 ) ;
call . timers . total35s = setTimeout ( ( ) => {
if ( ! calls . has ( callId ) ) return ;
if ( ! call . connectedAtMs ) {
void finalizeCall ( call , { localReasonCode : 'no_answer' , debugReason : 'total_timeout_35s' } ) ;
}
} , 35000 ) ;
await authService . callInviteBroadcast ( { toLogin : cleanPeer , callId , type : TYPES . INVITE } ) ;
try {
await authService . callInviteBroadcast ( { toLogin : cleanPeer , callId , type : TYPES . INVITE } ) ;
} catch ( error ) {
await finalizeCall ( call , { localReasonCode : 'error' , debugReason : ` invite_failed: ${ toErrorText ( error ) } ` } ) ;
throw error ;
}
}
export async function handleIncomingCallInvite ( evt ) {
@@ -314,12 +587,15 @@ export async function handleIncomingCallInvite(evt) {
callId ,
peerLogin : fromLogin ,
direction : 'in' ,
state : 'incoming' ,
phase : 'incoming' ,
statusText : ` Вам звонит ${ fromLogin } ` ,
remoteSessionId : fromSessionId ,
timers : { } ,
startedAtMs : nowMs ( ) ,
connectedAtMs : 0 ,
pc : null ,
localStream : null ,
muted : false ,
debugMode : false ,
debugRunId : '' ,
debugRole : '' ,
@@ -328,27 +604,38 @@ export async function handleIncomingCallInvite(evt) {
}
activeCallId = callId ;
setStatus ( call , ` входящий звонок о т ${ fromLogin } ` ) ;
setStatus ( call , ` Вам звони т ${ fromLogin } ` , 'incoming' ) ;
ensureIncomingNotification ( fromLogin ) ;
try {
await sendSignal ( call , TYPES . RINGING , 'ringing' ) ;
} catch { }
call . timers . incoming20s = setTimeout ( async ( ) => {
await sendSignal ( call , TYPES . TIMEOUT , 'timeout_20s' ) ;
await finishCall ( call , 'не ответили 20с ' , false ) ;
if ( ! calls . has ( callId ) ) return ;
try {
await sendSignal ( call , TYPES . TIMEOUT , 'timeout_20s' ) ;
} catch { }
await finalizeCall ( call , { localReasonCode : 'no_answer' , debugReason : 'incoming_timeout_20s' } ) ;
} , 20000 ) ;
}
const accepted = window . confirm ( ` Вам звонит ${ fromLogin } . Принять звонок? ` ) ;
if ( ! accepted ) {
await sendSignal ( call , TYPES . DECLINE _BUSY , 'decl ine ') ;
await finishCall ( call , 'отклонен' , false ) ;
return ;
}
call . state = 'accepted' ;
export async function acceptIncomingCall ( ) {
const call = getActiveCall ( ) ;
if ( ! call || call . direction !== 'in' || call . phase !== 'incom ing ') return ;
call . phase = 'connecting' ;
setStatus ( call , 'Соединяем…' , 'connecting' ) ;
cleanupTimers ( call ) ;
await sendSignal ( call , TYPES . ACCEPT , 'accept' ) ;
setStatus ( call , 'принят, ждём offer' ) ;
}
export async function declineIncomingCall ( ) {
const call = getActiveCall ( ) ;
if ( ! call || call . direction !== 'in' || call . phase !== 'incoming' ) return ;
try {
await sendSignal ( call , TYPES . DECLINE _BUSY , 'decline' ) ;
} catch { }
await finalizeCall ( call , { localReasonCode : 'declined' , debugReason : 'declined_by_user' } ) ;
}
export async function handleIncomingCallSignal ( evt ) {
@@ -365,30 +652,34 @@ export async function handleIncomingCallSignal(evt) {
if ( ! call . remoteSessionId ) call . remoteSessionId = fromSessionId ;
if ( type === TYPES . RINGING ) {
call . state = 'ring ing' ;
setStatus ( call , 'идут гудки ' ) ;
if ( call . direction === 'out' && call . phase === 'search ing') {
setStatus ( call , 'Вызываем…' , 'ringing ') ;
}
return ;
}
if ( type === TYPES . ACCEPT ) {
call . state = 'accepted ' ;
setStatus ( call , 'звонок принят ' ) ;
call . phase = 'connecting ' ;
setStatus ( call , 'Соединяем…' , 'connecting ') ;
await onAccept ( call ) ;
return ;
}
if ( type === TYPES . DECLINE _BUSY ) {
await finishCall ( call , 'занят/отклонено' , false ) ;
await finalizeCall ( call , { localReasonCode : 'busy' , debugReason : 'decline_or_busy' } ) ;
return ;
}
if ( type === TYPES . TIMEOUT ) {
await finishCall ( call , 'таймаут на стороне собеседника' , false ) ;
await finalizeCall ( call , { localReasonCode : 'no_answer' , debugReason : 'remote_timeout' } ) ;
return ;
}
if ( type === TYPES . HANGUP ) {
await finishCall ( call , 'собеседник завершил звонок' , false ) ;
await finalizeCall ( call , {
localReasonCode : call . connectedAtMs ? 'completed' : 'no_answer' ,
debugReason : 'remote_hangup' ,
} ) ;
return ;
}
@@ -399,11 +690,11 @@ export async function handleIncomingCallSignal(evt) {
const answer = await pc . createAnswer ( ) ;
await pc . setLocalDescription ( answer ) ;
await sendSignal ( call , TYPES . ANSWER , JSON . stringify ( answer ) ) ;
setStatus ( call , 'получен offer, отправлен answer ' ) ;
setStatus ( call , 'Соединяем…' , 'connecting ') ;
await emitDebug ( call , 'info' , 'offer_processed' , 'answer sent' ) ;
} catch ( error ) {
await emitDebug ( call , 'error' , 'offer_process_failed' , toErrorText ( error ) ) ;
throw error ;
await finalizeCall ( call , { localReasonCode : 'error' , debugReason : ` offer_failed: ${ toErrorText ( error ) } ` } ) ;
}
return ;
}
@@ -412,11 +703,11 @@ export async function handleIncomingCallSignal(evt) {
try {
const pc = await ensurePeerConnection ( call ) ;
await pc . setRemoteDescription ( new RTCSessionDescription ( JSON . parse ( data ) ) ) ;
setStatus ( call , 'получен answer ' ) ;
setStatus ( call , 'Соединяем…' , 'connecting ') ;
await emitDebug ( call , 'info' , 'answer_processed' , 'remote description set' ) ;
} catch ( error ) {
await emitDebug ( call , 'error' , 'answer_process_failed' , toErrorText ( error ) ) ;
throw error ;
await finalizeCall ( call , { localReasonCode : 'error' , debugReason : ` answer_failed: ${ toErrorText ( error ) } ` } ) ;
}
return ;
}
@@ -428,7 +719,6 @@ export async function handleIncomingCallSignal(evt) {
await emitDebug ( call , 'info' , 'ice_processed' , 'candidate added' ) ;
} catch ( error ) {
await emitDebug ( call , 'error' , 'ice_process_failed' , toErrorText ( error ) ) ;
throw error ;
}
}
}
@@ -436,5 +726,9 @@ export async function handleIncomingCallSignal(evt) {
export async function hangupActiveCall ( ) {
if ( ! activeCallId ) return ;
const call = getCall ( activeCallId ) ;
await finishCall ( call , 'сброшен пользователем' , true ) ;
await finalizeCall ( call , {
localReasonCode : call ? . connectedAtMs ? 'completed' : 'no_answer' ,
debugReason : 'hangup_by_user' ,
notifyRemoteHangup : true ,
} ) ;
}