kod-temp
Version:
Workano Communications SDK
1,210 lines (1,209 loc) • 62.2 kB
JavaScript
import { SessionState } from 'sip.js/lib/api/session-state';
import { Invitation } from 'sip.js/lib/api';
import CallSession from '../CallSession';
import Emitter from '../../utils/Emitter';
import IssueReporter from '../../service/IssueReporter';
export const ON_USER_AGENT = 'onUserAgent';
export const ON_REGISTERED = 'onRegistered';
export const ON_UNREGISTERED = 'onUnRegistered';
export const ON_PROGRESS = 'onProgress';
export const ON_CALL_ACCEPTED = 'onCallAccepted';
export const ON_CALL_ANSWERED = 'onCallAnswered';
export const ON_CALL_INCOMING = 'onCallIncoming';
export const ON_CALL_OUTGOING = 'onCallOutgoing';
export const ON_CALL_MUTED = 'onCallMuted';
export const ON_CALL_UNMUTED = 'onCallUnmuted';
export const ON_CALL_RESUMED = 'onCallResumed';
export const ON_CALL_HELD = 'onCallHeld';
export const ON_CALL_UNHELD = 'onCallUnHeld';
export const ON_CAMERA_DISABLED = 'onCameraDisabled';
export const ON_CAMERA_RESUMED = 'onCameraResumed';
export const ON_CALL_CANCELED = 'onCallCanceled';
export const ON_CALL_FAILED = 'onCallFailed';
export const ON_CALL_REJECTED = 'onCallRejected';
export const ON_CALL_ENDED = 'onCallEnded';
export const ON_CALL_ENDING = 'onCallEnding';
export const ON_MESSAGE = 'onMessage';
export const ON_REINVITE = 'reinvite';
export const ON_TRACK = 'onTrack';
export const ON_AUDIO_STREAM = 'onAudioStream';
export const ON_VIDEO_STREAM = 'onVideoStream';
export const ON_REMOVE_STREAM = 'onRemoveStream';
export const ON_SHARE_SCREEN_STARTED = 'onScreenShareStarted';
export const ON_SHARE_SCREEN_ENDING = 'onScreenShareEnding';
export const ON_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
export const ON_TERMINATE_SOUND = 'terminateSound';
export const ON_PLAY_RING_SOUND = 'playRingingSound';
export const ON_PLAY_INBOUND_CALL_SIGNAL_SOUND = 'playInboundCallSignalSound';
export const ON_PLAY_HANGUP_SOUND = 'playHangupSound';
export const ON_PLAY_PROGRESS_SOUND = 'playProgressSound';
export const ON_VIDEO_INPUT_CHANGE = 'videoInputChange';
export const ON_CALL_ERROR = 'onCallError';
export const ON_MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
export const ON_NETWORK_STATS = 'onNetworkStats';
export const ON_CHAT = 'phone/ON_CHAT';
export const ON_SIGNAL = 'phone/ON_SIGNAL';
export const ON_DISCONNECTED = 'onDisconnected';
export const ON_EARLY_MEDIA = 'onEarlyMedia';
export const MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
export const MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
export const events = [ON_USER_AGENT, ON_REGISTERED, ON_UNREGISTERED, ON_PROGRESS, ON_CALL_ACCEPTED, ON_CALL_ANSWERED, ON_CALL_INCOMING, ON_CALL_OUTGOING, ON_CALL_MUTED, ON_CALL_UNMUTED, ON_CALL_RESUMED, ON_CALL_HELD, ON_CALL_UNHELD, ON_CAMERA_DISABLED, ON_CALL_FAILED, ON_CALL_ENDED, ON_CALL_REJECTED, ON_MESSAGE, ON_REINVITE, ON_TRACK, ON_AUDIO_STREAM, ON_VIDEO_STREAM, ON_REMOVE_STREAM, ON_SHARE_SCREEN_ENDED, ON_TERMINATE_SOUND, ON_PLAY_RING_SOUND, ON_PLAY_INBOUND_CALL_SIGNAL_SOUND, ON_PLAY_HANGUP_SOUND, ON_PLAY_PROGRESS_SOUND, ON_VIDEO_INPUT_CHANGE, ON_SHARE_SCREEN_STARTED, ON_CALL_ERROR, ON_CHAT, ON_SIGNAL, ON_NETWORK_STATS, ON_DISCONNECTED, ON_EARLY_MEDIA];
const logger = IssueReporter.loggerFor('webrtc-phone');
export default class WebRTCPhone extends Emitter {
client;
allowVideo;
callSessions;
incomingSessions;
currentSipSession;
currentCallSession;
audioOutputDeviceId;
audioRingDeviceId;
audioOutputVolume;
audioRingVolume;
ringingEnabled;
acceptedSessions;
rejectedSessions;
ignoredSessions;
currentScreenShare;
lastScreenShare;
shouldSendReinvite;
constructor(client, audioOutputDeviceId, allowVideo = false, audioRingDeviceId) {
super();
this.client = client;
this.allowVideo = allowVideo;
this.callSessions = {};
this.audioOutputDeviceId = audioOutputDeviceId;
this.audioRingDeviceId = audioRingDeviceId || audioOutputDeviceId;
this.audioOutputVolume = 1;
this.audioRingVolume = 1;
this.incomingSessions = [];
this.ringingEnabled = true;
this.shouldSendReinvite = false;
this.bindClientEvents();
this.acceptedSessions = {};
this.rejectedSessions = {};
this.ignoredSessions = {};
}
register() {
logger.info('WebRTC register', { client: !!this.client });
if (!this.client) {
return Promise.resolve();
}
return this.client.register().then(() => {
logger.info('WebRTC register, registered');
return this.bindClientEvents();
}).catch(error => {
// Avoid exception on `t.server.scheme` in sip transport when losing the webrtc socket connection
console.error('register error', error, error.message, error.stack);
logger.error('WebRTC register error', {
message: error.message,
stack: error.stack,
});
});
}
unregister() {
if (!this.client || !this.client.isRegistered()) {
return null;
}
return this.client.unregister();
}
stop() {
if (!this.client) {
return;
}
logger.info('WebRTC phone stop');
this.client.stop();
}
removeIncomingSessions(id) {
this.incomingSessions = this.incomingSessions.filter(sessionId => sessionId !== id);
}
isWebRTC() {
return true;
}
async sendReinvite(callSession, constraints = null, conference = false, audioOnly = false, iceRestart = false) {
const sipSession = this.findSipSession(callSession);
logger.info('WebRTC phone - send reinvite', {
sessionId: sipSession ? sipSession.id : null,
constraints,
audioOnly,
conference,
iceRestart,
});
if (!sipSession) {
return;
}
const shouldScreenShare = constraints?.screen;
const isUpgrade = shouldScreenShare || constraints?.video;
// Downgrade
if (constraints && !isUpgrade) {
// No reinvite needed
this._downgradeToAudio(callSession);
return;
}
if (isUpgrade) {
const shouldReinvite = await this._upgradeToVideo(callSession, constraints, conference);
if (!shouldReinvite) {
return;
}
}
return this.client.reinvite(sipSession, constraints, conference, audioOnly, iceRestart);
}
getUserAgent() {
return this.client?.config?.userAgentString || 'webrtc-phone';
}
startHeartbeat() {
logger.info('WebRTC phone - start heartbeat', {
client: !!this.client,
hasHeartbeat: this.client.hasHeartbeat(),
});
if (!this.client || this.client.hasHeartbeat()) {
return;
}
this.client.startHeartbeat();
}
stopHeartbeat() {
logger.info('WebRTC phone - stopHeartbeat', {
client: !!this.client,
});
if (!this.client) {
return;
}
this.client.stopHeartbeat();
}
setOnHeartbeatTimeout(cb) {
this.client.setOnHeartbeatTimeout(cb);
}
setOnHeartbeatCallback(cb) {
this.client.setOnHeartbeatCallback(cb);
}
reconnect() {
logger.info('WebRTC phone - reconnect', {
client: !!this.client,
});
if (!this.client) {
return;
}
this.client.attemptReconnection();
}
getOptions() {
return {
accept: true,
decline: true,
mute: true,
hold: true,
transfer: true,
sendKey: true,
addParticipant: false,
record: true,
merge: true,
};
}
onConnect() {
if (!this.client) {
return Promise.reject(new Error('No webrtc client'));
}
return this.client.onConnect();
}
onDisconnect() {
if (!this.client) {
return Promise.reject(new Error('No webrtc client'));
}
return this.client.onDisconnect();
}
_downgradeToAudio(callSession, withMessage = true) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return false;
}
logger.info('Downgrade to audio', {
id: callSession ? callSession.getId() : null,
withMessage,
});
this.client.downgradeToAudio(sipSession);
if (callSession) {
callSession.cameraEnabled = false;
this._updateCallSession(callSession);
}
if (withMessage) {
this._sendReinviteMessage(callSession, false);
}
return true;
}
// Returns true if we need to send a re-INVITE request
async _upgradeToVideo(callSession, constraints, isConference) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return Promise.resolve(false);
}
logger.info('Upgrade to video', {
id: callSession ? callSession.getId() : null,
constraints,
isConference,
});
const shouldScreenShare = constraints && !!constraints.screen;
const desktop = constraints && constraints.desktop;
const options = sipSession.sessionDescriptionHandlerOptionsReInvite;
const wasAudioOnly = options?.audioOnly;
const newStream = await this.client.upgradeToVideo(sipSession, constraints, isConference);
if (callSession) {
callSession.cameraEnabled = true;
this._updateCallSession(callSession);
}
// If no stream is returned, it means we have to reinvite
if (!newStream) {
return true;
}
this._sendReinviteMessage(callSession, true);
if (shouldScreenShare) {
this._onScreenSharing(newStream, sipSession, callSession, false, desktop);
}
// We have to reinvite to change the direction on the bundle when upgrading from an audioOnly conference
if (isConference && wasAudioOnly) {
return true;
}
// No reinvite needed here
return false;
}
_sendReinviteMessage(callSession, isUpgrade) {
const sipSession = this.findSipSession(callSession);
logger.info('Sending reinvite message', {
id: callSession ? callSession.getId() : null,
isUpgrade,
});
// Have to send the message after a delay due to latency to update the remote peer
setTimeout(() => {
this.sendMessage(sipSession, JSON.stringify({
type: MESSAGE_TYPE_SIGNAL,
content: {
type: ON_MESSAGE_TRACK_UPDATED,
update: isUpgrade ? 'upgrade' : 'downgrade',
sipCallId: sipSession && this.getSipSessionId(sipSession),
callId: callSession ? callSession.callId : null,
number: callSession ? callSession.number : null,
callerNumber: callSession ? callSession.callerNumber : null,
},
}));
}, 2500);
}
_bindEvents(sipSession) {
const sipSessionId = this.getSipSessionId(sipSession);
if (sipSession instanceof Invitation) {
// Monkey patch to know when canceled with the CANCEL message
const onCancel = sipSession._onCancel.bind(sipSession);
sipSession._onCancel = (message) => {
logger.trace('on sip session canceled', {
callId: message.callId,
sipSessionId,
});
onCancel(message);
const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
const callSession = this._createCallSession(sipSession);
delete this.callSessions[sipSessionId];
this.eventEmitter.emit(ON_CALL_CANCELED, callSession, elsewhere);
};
}
sipSession.stateChange.addListener((newState) => {
switch (newState) {
case SessionState.Terminating:
logger.info('WebRTC phone - call terminating', {
sipId: sipSession.id,
});
this.eventEmitter.emit(ON_CALL_ENDING, this._createCallSession(sipSession));
break;
case SessionState.Terminated:
{
logger.info('WebRTC phone - call terminated', {
sipId: sipSession.id,
});
// Should be called before `_onCallTerminated` or the callCount will not decrement...
const callSession = this._createCallSession(sipSession);
callSession.endTime = new Date();
const wasCurrentSession = this._onCallTerminated(sipSession);
return this.eventEmitter.emit(ON_CALL_ENDED, callSession, wasCurrentSession);
}
default:
break;
}
});
if (!sipSession.sessionDescriptionHandler) {
return;
}
// Video events
const sdh = sipSession.sessionDescriptionHandler;
const peerConnection = sdh.peerConnection;
if (!peerConnection) {
logger.info('null peer connection');
return;
}
peerConnection.ontrack = (rawEvent) => {
const event = rawEvent;
const [stream] = event.streams;
const kind = event && event.track && event.track.kind;
logger.info('on track stream called on the peer connection', {
callId: this.getSipSessionId(sipSession),
streamId: stream ? stream.id : null,
tracks: stream ? stream.getTracks() : null,
kind,
});
if (kind === 'audio') {
return this.eventEmitter.emit(ON_AUDIO_STREAM, stream);
}
return this.eventEmitter.emit(ON_VIDEO_STREAM, stream, event.track.id, event, sipSession);
};
peerConnection.onremovestream = (event) => {
logger.info('on remove stream called on the peer connection', {
id: event.stream.id,
tracks: event.stream.getTracks(),
});
this.eventEmitter.emit(ON_REMOVE_STREAM, event.stream);
};
}
async startScreenSharing(constraints, callSession) {
logger.info('WebRTC - start screen sharing', {
constraints,
id: callSession ? callSession.getId() : null,
});
const screenShareStream = await this.client.getStreamFromConstraints(constraints);
if (!screenShareStream) {
throw new Error(`Can't create media stream for screensharing with: ${JSON.stringify(constraints)}`);
}
const screenTrack = screenShareStream.getVideoTracks()[0];
const sipSession = this.currentSipSession;
const pc = sipSession && this.client.getPeerConnection(this.getSipSessionId(sipSession));
const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
const localStream = sipSession && this.client.getLocalStream(this.getSipSessionId(sipSession));
const videoTrack = localStream ? localStream.getTracks().find(track => track.kind === 'video') : null;
const hadVideo = !!videoTrack;
// Stop local video tracks
if (videoTrack && localStream) {
videoTrack.enabled = false;
videoTrack.stop();
localStream.removeTrack(videoTrack);
}
if (localStream) {
localStream.addTrack(screenTrack);
}
if (sender) {
sender.replaceTrack(screenTrack);
}
this._onScreenSharing(screenShareStream, sipSession, callSession, hadVideo, constraints.desktop);
return screenShareStream;
}
async stopScreenSharing(restoreLocalStream = true, callSession) {
if (!this.currentScreenShare) {
return;
}
logger.info('WebRTC phone - stop screen sharing', {
restoreLocalStream,
});
let reinvited = null;
try {
if (this.currentScreenShare.stream) {
this.currentScreenShare.stream.getTracks()
.filter((track) => track.enabled && track.kind === 'video')
.forEach((track) => track.stop());
}
if (restoreLocalStream) {
const targetCallSession = callSession || this.currentCallSession;
const conference = this.isConference(targetCallSession);
// When stopping screenshare and we had video before that, we have to re-upgrade
// When upgrading directly to screenshare (eg: we don't have a videoLocalStream to replace)
// We have to downgrade to audio.
const screenshareStopped = this.currentScreenShare.hadVideo;
const constraints = {
audio: false,
video: screenshareStopped,
};
// We have to remove the video sender to be able to re-upgrade to video in `sendReinvite` below
this._downgradeToAudio(targetCallSession, false);
// Wait for the track to be removed, unless the video sender won't have a null track in `_upgradeToVideo`.
await new Promise(resolve => setTimeout(resolve, 500));
reinvited = await this.sendReinvite(targetCallSession, constraints, conference);
}
}
catch (e) {
console.warn(e);
}
const sipSession = this.currentSipSession;
this.eventEmitter.emit(ON_SHARE_SCREEN_ENDED, callSession && sipSession ? this._createCallSession(sipSession, callSession, {
screensharing: false,
}) : null);
this.currentScreenShare = null;
return reinvited;
}
_onScreenSharing(screenStream, sipSession, callSession, hadVideo, desktop) {
const screenTrack = screenStream.getVideoTracks()[0];
const sipSessionId = this.getSipSessionId(sipSession);
const pc = this.client.getPeerConnection(sipSessionId);
const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
logger.info('WebRTC phone - on screensharing', {
hadVideo,
id: this.getSipSessionId(sipSession),
screenTrack,
});
if (screenTrack) {
screenTrack.onended = () => {
logger.info('WebRTC phone - on screenshare ended', {
hadVideo,
id: this.getSipSessionId(sipSession),
screenTrack,
});
this.eventEmitter.emit(ON_SHARE_SCREEN_ENDING, this._createCallSession(sipSession, callSession));
};
}
this.currentScreenShare = {
stream: screenStream,
hadVideo,
sender,
sipSessionId,
desktop,
};
this.client.setLocalMediaStream(this.getSipSessionId(sipSession), screenStream);
this.eventEmitter.emit(ON_SHARE_SCREEN_STARTED, this._createCallSession(sipSession, callSession, {
screensharing: true,
}), screenStream);
}
_onCallAccepted(sipSession, cameraEnabled) {
logger.info('WebRTC phone - on call accepted', {
sipId: sipSession.id,
cameraEnabled,
});
const callSession = this._createAcceptedCallSession(sipSession, cameraEnabled);
if (this.currentSipSession && !this.isCurrentCallSipSession(callSession)) {
logger.info('WebRTC phone - on call accepted with another ongoing call, holding it', {
sipId: sipSession.id,
cameraEnabled,
});
this.holdSipSession(this.currentSipSession, this.currentCallSession, true);
}
this.currentSipSession = sipSession;
this.currentCallSession = callSession;
this.eventEmitter.emit(ON_TERMINATE_SOUND, callSession, 'call accepted');
const sipSessionId = this.getSipSessionId(sipSession);
if (sipSessionId) {
this.removeIncomingSessions(sipSessionId);
}
callSession.answerTime = new Date();
this._updateCallSession(callSession);
this.eventEmitter.emit(ON_CALL_ACCEPTED, callSession, cameraEnabled);
return callSession;
}
async changeAudioDevice(id) {
logger.info('WebRTC phone - change audio device', {
deviceId: id,
});
this.audioOutputDeviceId = id;
return this.client.changeAudioOutputDevice(id);
}
createAudioElementFor(sessionId) {
return this.client.createAudioElementFor(sessionId);
}
changeRingDevice(id) {
logger.info('WebRTC phone - changing ring device', {
id,
});
this.audioRingDeviceId = id;
}
// volume is a value between 0 and 1
changeAudioVolume(volume) {
logger.info('WebRTC phone - changing audio volume', {
volume,
});
this.audioOutputVolume = volume;
this.client.changeAudioOutputVolume(volume);
}
// volume is a value between 0 and 1
changeRingVolume(volume) {
logger.info('WebRTC phone - changing ring volume', {
volume,
});
this.audioRingVolume = volume;
}
changeAudioInputDevice(id, force) {
logger.info('WebRTC phone - changeAudio input device', {
deviceId: id,
});
return this.client.changeAudioInputDevice(id, this.currentSipSession, force);
}
async changeVideoInputDevice(id) {
logger.info('WebRTC phone - change video input device', {
deviceId: id,
});
return this.client.changeVideoInputDevice(id, this.currentSipSession);
}
changeSessionVideoInputDevice(id) {
logger.info('WebRTC phone - change session video input device', {
deviceId: id,
});
return this.currentSipSession && this.client.changeSessionVideoInputDevice(id, this.currentSipSession);
}
_onCallTerminated(sipSession) {
const sipSessionId = this.getSipSessionId(sipSession);
logger.info('WebRTC phone - on call terminated', {
sipId: sipSession.id,
sipSessionId,
ids: Object.keys(this.callSessions),
});
const callSession = this._createCallSession(sipSession);
const isCurrentSession = this.isCurrentCallSipSession(callSession);
const isCurrentIncomingCall = callSession.is(this.getIncomingCallSession());
// If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
// Otherwise, we have to re-trigger an incoming call event if another call is incoming
const shouldRetrigger = isCurrentSession ? this.incomingSessions.length : isCurrentIncomingCall;
// Terminate incoming sound only for the ringing call
// (so another terminated call won't stop the incoming call sound)
if (isCurrentIncomingCall) {
setTimeout(() => {
// Avoid race condition when the other is calling and hanging up immediately
this.eventEmitter.emit(ON_TERMINATE_SOUND, callSession, 'call terminated');
}, 5);
}
if (sipSessionId) {
this.removeIncomingSessions(sipSessionId);
}
delete this.callSessions[sipSessionId];
if (isCurrentSession) {
this.currentSipSession = undefined;
this.currentCallSession = undefined;
}
const hasIncomingCallSession = this.hasIncomingCallSession();
// Re-trigger incoming call event for remaining incoming calls
logger.info('WebRTC phone - check to re-trigger incoming call', {
sipId: sipSession.id,
shouldRetrigger,
hasIncomingCallSession,
});
this.client.onCallEnded(sipSession);
if (hasIncomingCallSession && shouldRetrigger) {
const nextCallSession = this.getIncomingCallSession();
// Avoid race condition
setTimeout(() => {
if (this.ringingEnabled) {
if (!this.currentCallSession) {
this.eventEmitter.emit(ON_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCallSession);
}
else {
this.eventEmitter.emit(ON_PLAY_INBOUND_CALL_SIGNAL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCallSession);
}
}
this.eventEmitter.emit(ON_CALL_INCOMING, nextCallSession, nextCallSession?.cameraEnabled);
}, 100);
}
if (callSession.getId() in this.ignoredSessions) {
return false;
}
// @ts-ignore: does not exist
if (!sipSession.isCanceled) {
setTimeout(() => {
this.eventEmitter.emit(ON_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, callSession);
}, 10);
}
return isCurrentSession;
}
setActiveSipSession(callSession) {
const sipSessionId = this.findSipSession(callSession);
if (!sipSessionId) {
return;
}
this.currentSipSession = sipSessionId;
this.currentCallSession = callSession;
}
hasAnActiveCall() {
return !!this.currentSipSession;
}
// /!\ In some case with react-native webrtc the session will have only one audio stream set
// Maybe due to https://github.com/react-native-webrtc/react-native-webrtc/issues/401
hasActiveRemoteVideoStream() {
const sipSession = this.currentSipSession;
if (!sipSession) {
return false;
}
const sdh = sipSession.sessionDescriptionHandler;
const peerConnection = sdh.peerConnection;
const remoteStream = peerConnection.getRemoteStreams().find((stream) => !!stream.getVideoTracks().length);
return remoteStream && remoteStream.getVideoTracks().some(track => !track.muted);
}
callCount() {
return Object.keys(this.callSessions).length;
}
isCurrentCallSipSession(callSession) {
if (!this.currentSipSession) {
return false;
}
return this.currentSipSession && this.getSipSessionId(this.currentSipSession) === callSession.getId();
}
hasVideo(callSession) {
return this.client.hasVideo(callSession.getId());
}
hasAVideoTrack(callSession) {
return this.client.hasAVideoTrack(callSession.getId());
}
// Deprecated
getLocalStreamForCall(callSession) {
logger.warn('WebRTCPhone.getLocalStreamForCall is deprecated, use WebRTCPhone.getLocalStream instead');
return this.getLocalStream(callSession);
}
// Deprecated
getRemoteStreamForCall(callSession) {
logger.warn('WebRTCPhone.getRemoteStreamForCall is deprecated, use WebRTCPhone.getRemoteStream instead');
return this.getRemoteVideoStream(callSession);
}
getRemoteStreamsForCall(callSession) {
logger.warn('WebRTCPhone.getRemoteStreamsForCall is deprecated, use WebRTCPhone.getRemoteStreams instead');
return this.getRemoteStream(callSession);
}
accept(callSession, cameraEnabled) {
logger.info('WebRTC phone - accept call', {
id: callSession ? callSession.getId() : 'n/a',
cameraEnabled,
});
if (this.currentSipSession && !this.isCurrentCallSipSession(callSession)) {
this.holdSipSession(this.currentSipSession, this.currentCallSession, true);
}
if (!callSession) {
logger.warn('no CallSession to accept.');
return Promise.resolve(null);
}
const sipSessionId = this.getSipSessionIdFromCallSession(callSession);
if (sipSessionId && !(sipSessionId in this.callSessions)) {
logger.warn('Call session already ended or not found, can\'t accept it.', {
sipSessionId,
});
return Promise.resolve(null);
}
if (callSession.getId() in this.acceptedSessions) {
logger.warn('CallSession already accepted.', {
id: callSession ? callSession.getId() : 'n/a',
});
return Promise.resolve(callSession.sipCallId);
}
this.shouldSendReinvite = false;
callSession.answerTime = new Date();
this._updateCallSession(callSession);
this.acceptedSessions[callSession.getId()] = true;
this.eventEmitter.emit(ON_CALL_ANSWERED, callSession);
const sipSession = this.client.getSipSession(callSession.getId());
if (sipSession) {
logger.info('accept call, session found', { sipId: sipSession.id });
return this.client.answer(sipSession, this.allowVideo ? cameraEnabled : false).then(() => {
return callSession.sipCallId;
}).catch(e => {
logger.error(e);
this.endCurrentCall(callSession);
throw e;
});
}
const error = 'No CallSession found to accept.';
logger.warn(error, {
id: callSession ? callSession.getId() : 'n/a',
ids: this.client.getSipSessionIds(),
});
if (callSession) {
this.eventEmitter.emit(ON_CALL_ERROR, new Error(error), callSession);
}
return Promise.resolve(null);
}
isAccepted(callSession) {
return callSession && callSession.getId() in this.acceptedSessions;
}
async reject(callSession) {
logger.info('reject WebRTC called', {
id: callSession.getId(),
});
this.eventEmitter.emit(ON_TERMINATE_SOUND, callSession, 'call rejected locally');
if (!callSession || callSession.getId() in this.rejectedSessions) {
return;
}
this.shouldSendReinvite = false;
this.rejectedSessions[callSession.getId()] = true;
const sipSession = this.findSipSession(callSession);
if (sipSession) {
logger.info('WebRTC rejecting', {
sipId: sipSession.id,
});
this.client.hangup(sipSession);
}
}
async ignore(callSession) {
logger.info('WebRTC ignore', {
id: callSession.getId(),
});
// kill the ring
this.eventEmitter.emit(ON_TERMINATE_SOUND, callSession, 'ignoring call');
this.ignoredSessions[callSession.getId()] = true;
callSession.ignore();
}
atxfer(callSession) {
const sipSession = this.findSipSession(callSession);
if (sipSession) {
logger.info('WebRTC atxfer', {
sipId: sipSession.id,
});
return this.client.atxfer(sipSession);
}
}
hold(callSession, withEvent = true) {
logger.info('WebRTC hold', {
id: callSession.getId(),
});
const sipSession = this.findSipSession(callSession);
return sipSession ? this.holdSipSession(sipSession, callSession, withEvent) : null;
}
// @Deprecated
unhold(callSession, withEvent = true) {
console.warn('Please note that `phone.unhold()` is being deprecated; `phone.resume()` is the preferred method');
return this.resume(callSession, withEvent);
}
resume(callSession, withEvent = true) {
logger.info('WebRTC resume called', {
id: callSession ? callSession.getId() : null,
});
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return new Promise((resolve, reject) => reject(new Error('No session to resume')));
}
logger.info('WebRTC resuming', {
sipId: sipSession.id,
});
// Hold current session if different from the current one (we don't want 2 sessions active at the same time).
if (this.currentSipSession && this.currentSipSession.id !== sipSession.id) {
logger.info('WebRTC hold call after resume', {
id: this.currentSipSession.id,
});
this.holdSipSession(this.currentSipSession, this.currentCallSession, withEvent);
}
const promise = this.unholdSipSession(sipSession, callSession, withEvent);
this.currentSipSession = sipSession;
if (callSession) {
this.currentCallSession = callSession;
}
return promise;
}
async holdSipSession(sipSession, callSession, withEvent = true) {
if (!sipSession) {
return new Promise((resolve, reject) => reject(new Error('No session to hold')));
}
const sessionId = this.getSipSessionId(sipSession);
const hasVideo = this.client.hasLocalVideo(sessionId);
logger.info('WebRTC hold sip session', {
sipId: sipSession.id,
hasVideo,
});
// Stop screenshare if needed
if (this.currentScreenShare && this.currentScreenShare.sipSessionId === sessionId) {
this.lastScreenShare = this.currentScreenShare;
await this.stopScreenSharing(false, callSession);
}
// Downgrade to audio if needed
if (hasVideo) {
await this.client.downgradeToAudio(sipSession);
}
const isConference = !!callSession && callSession.isConference();
const promise = this.client.hold(sipSession, isConference, hasVideo);
if (withEvent) {
this.eventEmitter.emit(ON_CALL_HELD, this._createCallSession(sipSession, callSession));
}
return promise;
}
async unholdSipSession(sipSession, callSession, withEvent = true) {
if (!sipSession) {
return new Promise((resolve, reject) => reject(new Error('No session to unhold')));
}
const isConference = !!callSession && callSession.isConference();
const sessionId = this.getSipSessionId(sipSession);
logger.info('WebRTC unhold sip session', {
sessionId,
isConference,
});
const { hasVideo, } = this.client.getHeldSession(sessionId) || {};
const wasScreensharing = this.lastScreenShare && this.lastScreenShare.sipSessionId === sessionId;
const wasDesktop = this.lastScreenShare && this.lastScreenShare.desktop;
const promise = this.client.unhold(sipSession, isConference);
if (hasVideo) {
const constraints = {
audio: false,
video: true,
screen: wasScreensharing,
desktop: wasDesktop,
};
await this.client.upgradeToVideo(sipSession, constraints, isConference);
}
const onScreenSharing = (stream) => {
const hadVideo = this.lastScreenShare && this.lastScreenShare.hadVideo;
if (stream) {
this._onScreenSharing(stream, sipSession, callSession, hadVideo);
}
this.lastScreenShare = null;
};
if (withEvent) {
// Calling webRtcClient.unhold will unmute the call; let's make sure the call session reflects that state
if (callSession) {
callSession.muted = false;
}
const updatedCallSession = this._createCallSession(sipSession, callSession);
// Deprecated event
this.eventEmitter.emit(ON_CALL_UNHELD, updatedCallSession);
this.eventEmitter.emit(ON_CALL_RESUMED, updatedCallSession);
}
return promise.then(() => {
const stream = callSession ? this.getLocalVideoStream(callSession) : null;
if (wasScreensharing) {
onScreenSharing(stream);
}
return stream;
});
}
mute(callSession, withEvent = true) {
logger.info('WebRTC mute called', {
id: callSession ? callSession.getId() : null,
});
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC muting', {
sipId: sipSession.id,
});
this.client.mute(sipSession);
const newCallSession = this._createCallSession(sipSession, callSession, {
muted: true,
});
if (this.currentCallSession && newCallSession.is(this.currentCallSession)) {
this.currentCallSession = newCallSession;
}
if (withEvent) {
this.eventEmitter.emit(ON_CALL_MUTED, newCallSession);
}
}
unmute(callSession, withEvent = true) {
logger.info('WebRTC unmute called', {
id: callSession ? callSession.getId() : null,
});
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC unmuting', {
sipId: sipSession.id,
});
this.client.unmute(sipSession);
const newCallSession = this._createCallSession(sipSession, callSession, {
muted: false,
});
if (this.currentCallSession && newCallSession.is(this.currentCallSession)) {
this.currentCallSession = newCallSession;
}
if (withEvent) {
this.eventEmitter.emit(ON_CALL_UNMUTED, newCallSession);
}
}
isAudioMuted(callSession) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return true;
}
return this.client.isAudioMuted(sipSession);
}
turnCameraOn(callSession) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC turn camera on', {
sipId: sipSession.id,
});
this.client.toggleCameraOn(sipSession);
this.eventEmitter.emit(ON_CAMERA_RESUMED, this._createCameraResumedCallSession(sipSession, callSession));
}
turnCameraOff(callSession) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC turn camera off', {
sipId: sipSession.id,
});
this.client.toggleCameraOff(sipSession);
this.eventEmitter.emit(ON_CAMERA_DISABLED, this._createCameraDisabledCallSession(sipSession, callSession));
}
sendKey(callSession, tone) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC send key', {
sipId: sipSession.id,
tone,
});
this.client.sendDTMF(sipSession, tone);
}
// Should be async to match CTIPhone definition
// @TODO: line is not used here
async makeCall(number, line, cameraEnabled, audioOnly = false, conference = false, options = {}) {
logger.info('make WebRTC call', {
number,
lineId: line ? line.id : null,
cameraEnabled,
audioOnly,
conference,
options,
});
if (!number) {
return Promise.resolve(null);
}
if (!this.client.isRegistered()) {
await this.client.register();
}
if (this.currentSipSession) {
this.holdSipSession(this.currentSipSession, this.currentCallSession, true).catch(e => {
logger.warn('Unable to hold current session when making another call', e);
});
}
let sipSession;
try {
sipSession = this.client.call(number, this.allowVideo ? cameraEnabled : false, audioOnly, conference, options);
this._bindEvents(sipSession);
}
catch (error) {
logger.warn('make WebRTC call, error', {
message: error.message,
stack: error.stack,
});
return Promise.resolve(null);
}
const callSession = this._createOutgoingCallSession(sipSession, cameraEnabled || false);
callSession.setIsConference(conference);
callSession.creationTime = new Date();
this._updateCallSession(callSession);
this.eventEmitter.emit(ON_PLAY_PROGRESS_SOUND, this.audioOutputDeviceId, this.audioOutputVolume);
this.currentSipSession = sipSession;
this.currentCallSession = callSession;
this.eventEmitter.emit(ON_CALL_OUTGOING, callSession);
// If an invite promise exists, catch exceptions on it to trigger error like OverConstraintsError.
// @ts-ignore: does not exist
if (sipSession.invitePromise) {
// @ts-ignore: does not exist
sipSession.invitePromise.catch(error => {
this.eventEmitter.emit(ON_CALL_ERROR, error, callSession);
});
}
return Promise.resolve(callSession);
}
transfer(callSession, target) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
return;
}
logger.info('WebRTC transfer', {
sipId: sipSession.id,
target,
});
this.client.transfer(sipSession, target);
}
async indirectTransfer(source, destination) {
const sipSession = this.client.getSipSession(source.sipCallId);
const sipSessionTarget = this.client.getSipSession(destination.sipCallId);
logger.info('WebRTC indirect transfer', {
sipId: sipSession ? sipSession.id : null,
target: sipSessionTarget ? sipSessionTarget.id : null,
});
if (!sipSessionTarget) {
return Promise.reject();
}
return new Promise(resolve => {
const options = {
requestDelegate: {
onAccept: () => {
// Wait a little so the REFER will take effect
setTimeout(() => {
this.hangup(destination).then(resolve);
}, 200);
},
},
};
return sipSessionTarget.refer(sipSession, options);
});
}
initiateCTIIndirectTransfer() { return Promise.resolve(null); }
cancelCTIIndirectTransfer() { }
confirmCTIIndirectTransfer() { }
async hangup(callSession) {
const sipSession = this.findSipSession(callSession);
if (!sipSession) {
console.error('Call is unknown to the WebRTC phone', callSession ? callSession.sipCallId : null, callSession ? callSession.callId : null);
return false;
}
const sipSessionId = this.getSipSessionId(sipSession);
logger.info('WebRTC hangup', {
sipId: sipSession.id,
sipSessionId,
});
await this.client.hangup(sipSession);
if (callSession) {
// Removal of `this.callSessions` will be done in `_onCallTerminated`.
this.endCurrentCall(callSession);
}
else if (sipSessionId) {
delete this.callSessions[sipSessionId];
}
this.shouldSendReinvite = false;
return true;
}
async getStats(callSession) {
const sipSession = this.findSipSession(callSession);
return sipSession ? this.client.getStats(sipSession) : null;
}
startNetworkMonitoring(callSession, interval = 1000) {
const sipSession = this.findSipSession(callSession);
return sipSession ? this.client.startNetworkMonitoring(sipSession, interval) : null;
}
stopNetworkMonitoring(callSession) {
const sipSession = this.findSipSession(callSession);
return sipSession ? this.client.stopNetworkMonitoring(sipSession) : null;
}
forceCancel(sipSession) {
// @ts-ignore: private
if (!sipSession || !sipSession.outgoingInviteRequest) {
return;
}
try {
// @ts-ignore: private
sipSession.outgoingInviteRequest.cancel();
}
catch (e) {
logger.info('force cancel, error', e);
}
}
endCurrentCall(callSession) {
const isCurrent = this.isCurrentCallSipSession(callSession);
logger.info('Ending current call', {
id: callSession ? callSession.getId() : null,
isCurrent,
});
if (isCurrent) {
this.currentSipSession = undefined;
this.currentCallSession = undefined;
}
this.eventEmitter.emit(ON_TERMINATE_SOUND, callSession, 'locally ended');
}
onConnectionMade() { }
isConference(callSession) {
if (!callSession) {
return false;
}
return this.client && this.client.isConference(callSession.sipCallId);
}
async close(force = false) {
logger.info('WebRTC close');
try {
await Promise.race([
this.unregister(),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Unregister, timed out')), 3000)),
]);
}
catch (e) {
logger.error('WebRTC close, unregister error', e);
}
await this.client.close(force);
this.unbind();
this.incomingSessions = [];
this.currentSipSession = undefined;
this.currentCallSession = undefined;
this.shouldSendReinvite = false;
this.rejectedSessions = {};
}
isRegistered() {
return this.client && this.client.isRegistered();
}
enableRinging() {
logger.info('WebRTC enable ringing');
this.ringingEnabled = true;
}
disableRinging() {
logger.info('WebRTC disable ringing');
this.ringingEnabled = false;
}
getCurrentCallSession() {
if (!this.currentSipSession) {
return null;
}
return this._createCallSession(this.currentSipSession, this.currentCallSession);
}
hasIncomingCallSession() {
return this.incomingSessions.length > 0;
}
getIncomingCallSession() {
if (!this.hasIncomingCallSession()) {
return null;
}
const sessionId = this.incomingSessions[0];
const sipSession = this.client.getSipSession(sessionId);
if (!sipSession) {
return null;
}
return this._createCallSession(sipSession);
}
// eslint-disable-next-line @typescript-eslint/default-param-last
sendMessage(sipSession = null, body, contentType = 'text/plain') {
return this.client.sendMessage(sipSession, body, contentType);
}
getLocalStream(callSession) {
return callSession ? this.client.getLocalStream(callSession.sipCallId) : null;
}
getLocalVideoStream(callSession) {
return callSession ? this.client.getLocalVideoStream(callSession.sipCallId) : null;
}
// Deprecated
getLocalMediaStream(callSession) {
logger.warn('WebRTCPhone.getLocalMediaStream is deprecated, use WebRTCPhone.getLocalStream instead');
return this.getLocalStream(callSession);
}
hasLocalVideo(callSession) {
return callSession ? this.client.hasLocalVideo(callSession.sipCallId) : false;
}
hasALocalVideoTrack(callSession) {
return callSession ? this.client.hasALocalVideoTrack(callSession.sipCallId) : false;
}
getRemoteStream(callSession) {
return callSession ? this.client.getRemoteStream(callSession.sipCallId) : null;
}
getRemoteVideoStream(callSession) {
return callSession ? this.client.getRemoteVideoStream(callSession.sipCallId) : null;
}
getRemoteVideoStreamFromPc(callSession) {
return callSession ? this.client.getRemoteVideoStreamFromPc(callSession.sipCallId) : null;
}
hasRemoteVideo(callSession) {
return callSession ? this.client.hasRemoteVideo(callSession.sipCallId) : false;
}
setMediaConstraints(media) {
this.client.setMediaConstraints(media);
}
isVideoRemotelyHeld(callSession) {
return callSession ? this.client.isVideoRemotelyHeld(callSession.sipCallId) : false;
}
bindClientEvents() {
this.client.unbind();
this.client.on(this.client.INVITE, (sipSession, wantsToDoVideo) => {
const autoAnswer = sipSession.request.getHeader('Answer-Mode') === 'Auto';
const withVideo = this.allowVideo ? wantsToDoVideo : false;
logger.info('WebRTC invite received', {
sipId: sipSession.id,
withVideo,
autoAnswer,
});
const callSession = this._createIncomingCallSession(sipSession, withVideo, null, autoAnswer);
this.incomingSessions.push(callSession.getId());
this._bindEvents(sipSession);
this.client.storeSipSession(sipSession);
callSession.creationTime = new Date();
this._updateCallSession(callSession);
this.eventEmitter.emit(ON_CALL_INCOMING, callSession, wantsToDoVideo);
if (!this.currentSipSession) {
if (this.ringingEnabled) {
this.eventEmitter.emit(ON_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, callSession);
}
}
else {
this.eventEmitter.emit(ON_PLAY_INBOUND_CALL_SIGNAL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, callSession);
}