import { CallingState } from '../store'; import { Call } from '../Call'; import { CallAcceptedEvent, CallRejectedEvent, OwnCapability, } from '../gen/coordinator'; import { CallEnded } from '../gen/video/sfu/event/events'; import { CallEndedReason } from '../gen/video/sfu/models/models'; /** * Event handler that watched the delivery of `call.accepted`. * Once the event is received, the call is joined. */ export const watchCallAccepted = (call: Call) => { return async function onCallAccepted(event: CallAcceptedEvent) { // We want to discard the event if it's from the current user if (event.user.id === call.currentUserId) return; const { state } = call; if ( event.call.created_by.id === call.currentUserId && state.callingState === CallingState.RINGING ) { await call.join(); } }; }; /** * Event handler that watches delivery of `call.rejected` Websocket event. * Once the event is received, the call is left. */ export const watchCallRejected = (call: Call) => { return async function onCallRejected(event: CallRejectedEvent) { // We want to discard the event if it's from the current user if (event.user.id === call.currentUserId) return; const { call: eventCall } = event; const { session: callSession } = eventCall; if (!callSession) { call.logger.warn( 'No call session provided. Ignoring call.rejected event.', event, ); return; } const rejectedBy = callSession.rejected_by; const { members, callingState } = call.state; if (callingState !== CallingState.RINGING) { call.logger.info( 'Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.', event, ); return; } if (call.isCreatedByMe) { const everyoneElseRejected = members .filter((m) => m.user_id !== call.currentUserId) .every((m) => rejectedBy[m.user_id]); if (everyoneElseRejected) { call.logger.info('everyone rejected, leaving the call'); await call.leave({ reject: true, reason: 'cancel', message: 'ring: everyone rejected', }); } } else { if (rejectedBy[eventCall.created_by.id]) { call.logger.info('call creator rejected, leaving call'); globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); await call.leave({ message: 'ring: creator rejected' }); } } }; }; /** * Event handler that watches the delivery of `call.ended` Websocket event. */ export const watchCallEnded = (call: Call) => { return function onCallEnded() { globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); const { callingState } = call.state; if ( callingState !== CallingState.IDLE && callingState !== CallingState.LEFT ) { call .leave({ message: 'call.ended event received', reject: false }) .catch((err) => { call.logger.error('Failed to leave call after call.ended ', err); }); } }; }; /** * Watches for `callEnded` events. */ export const watchSfuCallEnded = (call: Call) => { return call.on('callEnded', async (e: CallEnded) => { if (call.state.callingState === CallingState.LEFT) return; try { if (e.reason === CallEndedReason.LIVE_ENDED) { call.state.setBackstage(true); // don't leave the call if the user has permission to join backstage const { hasPermission } = call.permissionsContext; if (hasPermission(OwnCapability.JOIN_BACKSTAGE)) return; } // `call.ended` event arrived after the call is already left // and all event handlers are detached. We need to manually // update the call state to reflect the call has ended. call.state.setEndedAt(new Date()); const reason = CallEndedReason[e.reason]; globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote'); await call.leave({ message: `callEnded received: ${reason}` }); } catch (err) { call.logger.error( 'Failed to leave call after being ended by the SFU', err, ); } }); };