@voicenter-team/vsip
Version:
A vue based jssip wrapper.
995 lines (871 loc) • 43.4 kB
JavaScript
import JsSIP from 'jssip';
import {forEach} from 'p-iteration';
import WebRTCMetrics from "./helpers/webrtcmetrics/index";
import {setupTime} from "./helpers/time.helper"
import {filterObjectkeys} from "./helpers/filter.helper";
import {
STORAGE_KEYS,
STORE_MUTATION_TYPES,
CALL_EVENT_LISTENER_TYPE,
CONSTRAINTS,
CALL_KEYS_TO_INCLUDE,
METRIC_KEYS_TO_INCLUDE
} from './config/enum'
/**
* @typedef {Object} VSIPOptions
* @property {Object} store - Vuex store
*/
/**
* @typedef {Object} RoomInfo
* @property {Date} started
* @property {Number} roomId
*/
/**
* @typedef {Object} RoomList
* @property {RoomInfo} roomId
*/
let UA;
function simplifyCallObject(call) {
let simplified = {};
CALL_KEYS_TO_INCLUDE.forEach(key => {
if (call[key] !== undefined) {
simplified[key] = call[key]
}
})
return simplified
}
function getNewRoomId(activeRooms) {
const roomIdList = Object.keys(activeRooms);
if (roomIdList.length === 0) {
return 1;
}
return (parseInt(roomIdList.sort()[roomIdList.length - 1]) + 1);
}
function syncStream(event, call, outputDevice, volume) {
const audio = document.createElement('audio');
audio.id = call._id;
audio.class = 'audioTag';
audio.srcObject = event.stream;
audio.setSinkId(outputDevice);
audio.volume = volume;
audio.play();
call.audioTag = audio;
}
function processAudioVolume(stream, volume) {
let audioContext = new AudioContext()
let audioSource = audioContext.createMediaStreamSource(stream)
let audioDestination = audioContext.createMediaStreamDestination();
let gainNode = audioContext.createGain();
audioSource.connect(gainNode);
gainNode.connect(audioDestination);
gainNode.gain.value = volume;
return audioDestination.stream
}
let activeCalls = {};
/**
* @param {VSIPOptions} options
*/
function initStoreModule(options) {
if (!options || !options.store) {
throw new Error('Please initialise plugin with a Vuex store.');
}
options.store.registerModule('vsip', {
namespaced: true,
state: {
activeCalls: {},
/** @type RoomList */
activeRooms: {},
availableMediaDevices: [],
selectedMediaDevices: {
input: localStorage.getItem(STORAGE_KEYS.SELECTED_INPUT_DEVICE) || 'default',
output: localStorage.getItem(STORAGE_KEYS.SELECTED_OUTPUT_DEVICE) || 'default'
},
currentActiveRoomId: null,
muteWhenJoin: false,
uaInit: false,
sipDomain: '',
sipOptions: {},
listeners: {},
callAddingInProgress: null,
isDND: false,
isMuted: false,
originalStream: null,
microphoneInputLevel: 2, // from 0 to 2
speakerVolume: 1, // from 0 to 1
metricConfig: {
refreshEvery: 1000,
},
callTime: {},
callMetrics: {},
callStatus: {},
timeIntervals: {}
},
mutations: {
[STORE_MUTATION_TYPES.SET_DND]: (state, value) => {
state.isDND = value;
},
[STORE_MUTATION_TYPES.SET_METRIC_CONFIG]: (state, config) => {
state.metricConfig = {...state.metricConfig, ...config};
},
[STORE_MUTATION_TYPES.SET_MUTED]: (state, value) => {
state.isMuted = value;
},
[STORE_MUTATION_TYPES.SET_ORIGINAL_STREAM]: (state, value) => {
state.originalStream = value;
},
[STORE_MUTATION_TYPES.CALL_ADDING_IN_PROGRESS]: (state, value) => {
state.callAddingInProgress = value;
},
[STORE_MUTATION_TYPES.SET_MUTED_WHEN_JOIN]: (state, value) => {
state.muteWhenJoin = value;
},
[STORE_MUTATION_TYPES.ADD_LISTENER]: (state, {type, listener}) => {
const isListenerEmpty = !state.listeners[type] || !state.listeners[type].length
const newListeners = isListenerEmpty? [listener]: [...state.listeners[type], listener]
state.listeners = {
...state.listeners,
[type]: newListeners
}
},
[STORE_MUTATION_TYPES.REMOVE_LISTENER]: (state, value) => {
const listenersCopy = {...state.listeners};
delete listenersCopy[value];
state.listeners = {
...listenersCopy,
}
},
[STORE_MUTATION_TYPES.SET_MEDIA_DEVICES]: (state, value) => {
state.availableMediaDevices = value;
},
[STORE_MUTATION_TYPES.SET_UA_INIT]: (state) => {
state.uaInit = true
},
[STORE_MUTATION_TYPES.SET_SELECTED_INPUT_DEVICE]: (state, value) => {
localStorage.setItem(STORAGE_KEYS.SELECTED_INPUT_DEVICE, value);
state.selectedMediaDevices.input = value;
},
[STORE_MUTATION_TYPES.SET_SELECTED_OUTPUT_DEVICE]: (state, value) => {
localStorage.setItem(STORAGE_KEYS.SELECTED_OUTPUT_DEVICE, value);
state.selectedMediaDevices.output = value;
},
[STORE_MUTATION_TYPES.SET_MICROPHONE_INPUT_LEVEL]: (state, value) => {
state.microphoneInputLevel = value;
},
[STORE_MUTATION_TYPES.SET_SPEAKER_VOLUME]: (state, value) => {
state.speakerVolume = value;
},
[STORE_MUTATION_TYPES.UPDATE_CALL]: (state, value) => {
state.activeCalls = {
...state.activeCalls,
[value._id]: simplifyCallObject(value)
}
},
[STORE_MUTATION_TYPES.ADD_CALL]: (state, value) => {
state.activeCalls = {
...state.activeCalls,
[value._id]: simplifyCallObject(value)
}
activeCalls[value._id] = value
},
[STORE_MUTATION_TYPES.REMOVE_CALL]: (state, value) => {
const stateActiveCallsCopy = {...state.activeCalls};
delete stateActiveCallsCopy[value];
delete activeCalls[value];
state.activeCalls = {
...stateActiveCallsCopy,
}
},
[STORE_MUTATION_TYPES.SET_CALL_TIME]: (state, value) => {
const time = { ...value }
delete time['callId']
state.callTime = {
...state.callTime,
[value.callId]: time
}
},
[STORE_MUTATION_TYPES.REMOVE_CALL_TIME]: (state, callId) => {
const callTimeCopy = {...state.callTime};
delete callTimeCopy[callId];
state.callTime = {
...callTimeCopy,
}
},
[STORE_MUTATION_TYPES.SET_TIME_INTERVAL]: (state, {callId, interval}) => {
state.timeIntervals = {
...state.timeIntervals,
[callId]: interval
}
},
[STORE_MUTATION_TYPES.REMOVE_TIME_INTERVAL]: (state, callId) => {
const timeIntervalsCopy = {...state.timeIntervals};
clearInterval(timeIntervalsCopy[callId]);
delete timeIntervalsCopy[callId];
state.timeIntervals = {
...timeIntervalsCopy,
}
},
[STORE_MUTATION_TYPES.SET_CALL_METRICS]: (state, value) => {
const metrics = { ...value }
delete metrics['callId']
state.callMetrics = {
...state.callMetrics,
[value.callId]: metrics
}
},
[STORE_MUTATION_TYPES.REMOVE_CALL_METRICS]: (state, callId) => {
const callMetricsCopy = {...state.callMetrics};
delete callMetricsCopy[callId];
state.callMetrics = {
...callMetricsCopy,
}
},
[STORE_MUTATION_TYPES.ADD_CALL_STATUS]: (state, callId) => {
state.callStatus = {
...state.callStatus,
[callId]: {
isMoving: false,
isTransferring: false,
isMerging: false
}
}
},
[STORE_MUTATION_TYPES.UPDATE_CALL_STATUS]: (state, value) => {
const prevStatus = state.callStatus[value.callId]
const newStatus = { ...value }
delete newStatus['callId']
state.callStatus = {
...state.callStatus,
[value.callId]: {
...prevStatus,
...newStatus
}
}
},
[STORE_MUTATION_TYPES.REMOVE_CALL_STATUS]: (state, callId) => {
const callStatusCopy = {...state.callStatus};
delete callStatusCopy[callId];
state.callStatus = {
...callStatusCopy,
}
},
/**
* @param state
* @param {RoomInfo} value
*/
[STORE_MUTATION_TYPES.ADD_ROOM]: (state, value) => {
state.activeRooms = {
...state.activeRooms,
[value.roomId]: value
}
},
[STORE_MUTATION_TYPES.UPDATE_ROOM]: (state, value) => {
const room = state.activeRooms[value.roomId]
state.activeRooms = {
...state.activeRooms,
[value.roomId]: {
...room,
...value
}
}
},
[STORE_MUTATION_TYPES.REMOVE_ROOM]: (state, value) => {
const activeRoomsCopy = {...state.activeRooms};
delete activeRoomsCopy[value];
state.activeRooms = {
...activeRoomsCopy,
}
},
[STORE_MUTATION_TYPES.SET_CURRENT_ACTIVE_ROOM_ID]: (state, value) => {
state.currentActiveRoomId = value;
},
[STORE_MUTATION_TYPES.SET_SIP_DOMAIN]: (state, value) => {
state.sipDomain = value
},
[STORE_MUTATION_TYPES.SET_SIP_OPTIONS]: (state, value) => {
state.sipOptions = value
}
},
getters: {
getActiveRooms: state => state.activeRooms,
getActiveCalls: state => state.activeCalls,
getActiveCallsList: state => Object.values(state.activeCalls),
_uaInit: state => state.uaInit,
getSipDomain: state => state.sipDomain,
getSipOptions: (state, getters) => {
return {
...state.sipOptions,
mediaConstraints: getters.getUserMediaConstraints,
}
},
getInputDeviceList: (state) => {
return state.availableMediaDevices.filter(device => device.kind === 'audioinput');
},
getOutputDeviceList: (state) => {
return state.availableMediaDevices.filter(device => device.kind === 'audiooutput');
},
getCurrentActiveRoomId: state => state.currentActiveRoomId,
getSelectedInputDevice: state => state.selectedMediaDevices.input,
getInputDefaultDevice: (state, getters) => {
return getters.getInputDeviceList.find(device => device.id === 'default')
},
getOutputDefaultDevice: (state, getters) => {
return getters.getOutputDeviceList.find(device => device.id === 'default')
},
getSelectedOutputDevice: state => state.selectedMediaDevices.output,
getUserMediaConstraints: (state) => {
return {
audio: {
deviceId: {
exact: state.selectedMediaDevices.input
}
},
video: false
}
},
getListeners: state => state.listeners,
callAddingInProgress: state => state.callAddingInProgress,
isDND: state => state.isDND,
isMuted: state => state.isMuted,
metricConfig: state => state.metricConfig,
originalStream: state => state.originalStream,
microphoneInputLevel: state => state.microphoneInputLevel,
speakerVolume: state => state.speakerVolume,
callStatus: state => state.callStatus,
callTime: state => state.callTime,
callMetrics: state => state.callMetrics,
muteWhenJoin: state => state.muteWhenJoin
},
actions: {
async _addCall({commit, getters, dispatch}, session) {
if (Object.keys(getters.getActiveCalls).find(activeSession => activeSession._id === session._id) !== undefined) {
return;
}
const roomId = getNewRoomId(getters.getActiveRooms);
const newRoomInfo = {
started: new Date(),
incomingInProgress: false,
roomId
};
if (session.direction === CONSTRAINTS.CALL_DIRECTION_INCOMING) {
newRoomInfo["incomingInProgress"] = true;
dispatch('subscribe', {type: CALL_EVENT_LISTENER_TYPE.CALL_CONFIRMED, listener: (call) => {
if (session._id === call._id) {
commit(STORE_MUTATION_TYPES.UPDATE_ROOM, {
incomingInProgress: false,
roomId
});
dispatch('_startCallTimer', session._id);
}
}})
dispatch('subscribe', {type: CALL_EVENT_LISTENER_TYPE.CALL_FAILED, listener: (call) => {
if (session._id === call._id) {
commit(STORE_MUTATION_TYPES.UPDATE_ROOM, {
incomingInProgress: false,
roomId
});
}
}})
} else if (session.direction === CONSTRAINTS.CALL_DIRECTION_OUTGOING) {
dispatch('_startCallTimer', session._id);
}
session.roomId = roomId;
session.localMuted = false;
commit(STORE_MUTATION_TYPES.ADD_CALL, session);
commit(STORE_MUTATION_TYPES.ADD_CALL_STATUS, session._id);
commit(STORE_MUTATION_TYPES.ADD_ROOM, newRoomInfo);
},
_startCallTimer({commit, getters}, callId) {
const timeData = {
callId,
hours: 0,
minutes: 0,
seconds: 0,
formatted: ''
}
commit(STORE_MUTATION_TYPES.SET_CALL_TIME, timeData);
const interval = setInterval(() => {
const callTime = { ...getters.callTime[callId] };
const updatedTime = setupTime(callTime)
commit(STORE_MUTATION_TYPES.SET_CALL_TIME, { callId, ...updatedTime });
}, 1000)
commit(STORE_MUTATION_TYPES.SET_TIME_INTERVAL, { callId, interval });
},
_stopCallTimer({commit, getters}, callId) {
commit(STORE_MUTATION_TYPES.REMOVE_TIME_INTERVAL, callId);
commit(STORE_MUTATION_TYPES.REMOVE_CALL_TIME, callId);
},
_activeCallListRemove({commit, dispatch}, {_id}) {
const callRoomIdToConfigure = activeCalls[_id].roomId;
commit(STORE_MUTATION_TYPES.REMOVE_CALL, _id);
dispatch('_roomReconfigure', callRoomIdToConfigure);
},
_deleteRoomIfEmpty({commit, getters}, roomId) {
if (Object.values(activeCalls).filter(call => call.roomId === roomId).length === 0) {
commit(STORE_MUTATION_TYPES.REMOVE_ROOM, roomId)
if (getters.getCurrentActiveRoomId === roomId) {
commit(STORE_MUTATION_TYPES.SET_CURRENT_ACTIVE_ROOM_ID, roomId);
}
}
},
doMute({commit, dispatch, getters}, muted) {
const activeRoomId = getters.getCurrentActiveRoomId
commit(STORE_MUTATION_TYPES.SET_MUTED, muted);
dispatch('_roomReconfigure', activeRoomId)
},
_muteReconfigure({getters}, call) {
if (getters.isMuted) {
call.mute({audio: true})
} else {
call.unmute({audio: true})
}
},
_setOriginalStream({commit}, stream) {
commit(STORE_MUTATION_TYPES.SET_ORIGINAL_STREAM, stream);
},
setMuteWhenJoin({commit}, value) {
commit(STORE_MUTATION_TYPES.SET_MUTED_WHEN_JOIN, value);
},
setMicrophoneInputLevel({dispatch, commit, getters}, value) {
commit(STORE_MUTATION_TYPES.SET_MICROPHONE_INPUT_LEVEL, value);
dispatch('_roomReconfigure', getters.getCurrentActiveRoomId)
},
setSpeakerVolume({commit, getters}, value) {
commit(STORE_MUTATION_TYPES.SET_SPEAKER_VOLUME, value);
Object.values(activeCalls).forEach((call) => {
call.audioTag.volume = getters.speakerVolume
})
},
muteCaller({commit, getters, dispatch}, {callId, value}) {
const call = activeCalls[callId];
if (call && call.connection.getReceivers().length) {
call.localMuted = value
call.connection.getReceivers().forEach(receiver => {
receiver.track.enabled = !value
});
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
dispatch('_roomReconfigure', call.roomId)
}
},
async _roomReconfigure({commit, getters, dispatch}, roomId) {
if (!roomId) {
return;
}
const callsInRoom = Object.values(activeCalls).filter(call => call.roomId === roomId);
// Lets take care on the audio output first and check if passed room is our selected room
if (getters.getCurrentActiveRoomId === roomId) {
callsInRoom.forEach(call => {
if (call.audioTag) {
dispatch('_muteReconfigure', call);
call.audioTag.muted = false;
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
}
})
} else {
callsInRoom.forEach(call => {
call.audioTag.muted = true;
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
});
}
// Now lets configure the sound we are sending for each active call on this room
if (callsInRoom.length === 0) {
dispatch('_deleteRoomIfEmpty', roomId);
} else if (callsInRoom.length === 1 && getters.getCurrentActiveRoomId !== roomId) {
if (!callsInRoom[0]._localHold) {
dispatch('doCallHold', {callId: callsInRoom[0]._id, toHold: true, automatic: true})
}
} else if (callsInRoom.length === 1 && getters.getCurrentActiveRoomId === roomId) {
if (callsInRoom[0]._localHold && callsInRoom[0]._automaticHold) {
dispatch('doCallHold', {callId: callsInRoom[0]._id, toHold: false})
}
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
} catch (err) {
console.error(err)
}
if (callsInRoom[0].connection && callsInRoom[0].connection.getSenders()[0]) {
const processedStream = processAudioVolume(stream, getters.microphoneInputLevel)
processedStream.getTracks().forEach(track => track.enabled = !getters.isMuted)
dispatch('_setOriginalStream', processedStream);
await callsInRoom[0].connection.getSenders()[0].replaceTrack(processedStream.getTracks()[0]);
dispatch('_muteReconfigure', callsInRoom[0]);
}
} else if (callsInRoom.length > 1) {
await dispatch('_doConference', callsInRoom);
}
},
async _doConference({dispatch, getters}, sessions) {
sessions.forEach(call => {
if (call._localHold) {
dispatch('doCallHold', {callId: call._id, toHold: false})
}
});
// Take all received tracks from the sessions you want to merge
let receivedTracks = [];
sessions.forEach(session => {
if (session !== null && session !== undefined) {
session.connection.getReceivers().forEach(receiver => {
receivedTracks.push(receiver.track);
});
}
});
// Use the Web Audio API to mix the received tracks
const audioContext = new AudioContext();
const allReceivedMediaStreams = new MediaStream();
// For each call we will build dedicated mix for all other calls
await forEach(sessions, async session => {
if (session === null || session === undefined) {
return
}
const mixedOutput = audioContext.createMediaStreamDestination();
session.connection.getReceivers().forEach(receiver => {
receivedTracks.forEach(track => {
allReceivedMediaStreams.addTrack(receiver.track);
if (receiver.track.id !== track.id) {
let sourceStream = audioContext.createMediaStreamSource(new MediaStream([track]));
sourceStream.connect(mixedOutput);
}
});
});
if (sessions[0].roomId === getters.getCurrentActiveRoomId) {
// Mixing your voice with all the received audio
const stream = await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
const processedStream = processAudioVolume(stream, getters.microphoneInputLevel)
processedStream.getTracks().forEach(track => track.enabled = !getters.isMuted);
dispatch('_setOriginalStream', processedStream);
const sourceStream = audioContext.createMediaStreamSource(processedStream);
// stream.getTracks().forEach(track => track.enabled = !getters.isMuted) // TODO: Fix this
sourceStream.connect(mixedOutput);
}
if (session.connection.getSenders()[0]) {
//mixedOutput.stream.getTracks().forEach(track => track.enabled = !getters.isMuted) // Uncomment to mute all callers on mute
await session.connection.getSenders()[0].replaceTrack(mixedOutput.stream.getTracks()[0]);
dispatch('_muteReconfigure', session);
}
});
},
_triggerListener({getters}, {listenerType, session, event}) {
const listeners = getters.getListeners[listenerType];
if (!listeners || !listeners.length) {
return
}
listeners.forEach((listener) => {
listener(session, event);
});
},
_cancelAllOutgoingUnanswered({getters, dispatch}) {
getters.getActiveCallsList.filter(call => {
return call.direction === CONSTRAINTS.CALL_DIRECTION_OUTGOING
&& call.status === CONSTRAINTS.CALL_STATUS_UNANSWERED
}).forEach(call => dispatch('callTerminate', call._id));
},
async updateDeviceList({commit, dispatch, getters}) {
await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
const devices = await navigator.mediaDevices.enumerateDevices();
commit(STORE_MUTATION_TYPES.SET_MEDIA_DEVICES, devices);
},
async setMediaDevices({commit, dispatch, getters}, setDefaults = false) {
await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
const devices = await navigator.mediaDevices.enumerateDevices();
commit(STORE_MUTATION_TYPES.SET_MEDIA_DEVICES, devices);
const defaultMicrophone = setDefaults
? getters.getInputDefaultDevice.id
: ''
const defaultSpeaker = setDefaults
? getters.getOutputDefaultDevice.id
: ''
dispatch('setMicrophone', defaultMicrophone);
dispatch('setSpeaker', defaultSpeaker);
navigator.mediaDevices.addEventListener(
'devicechange',
() => {
dispatch('updateDeviceList')
}
)
},
async setMicrophone({commit, getters, dispatch}, dId) {
if (!getters.getInputDeviceList.find(({deviceId}) => deviceId === dId)) {
return
}
commit(STORE_MUTATION_TYPES.SET_SELECTED_INPUT_DEVICE, dId);
let stream = null;
try {
stream = await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
} catch (err) {
console.error(err);
}
if (Object.keys(getters.getActiveCalls).length === 0) {
return;
}
const callsInCurrentRoom = Object.values(activeCalls).filter(call => call.roomId === getters.getCurrentActiveRoomId);
if (callsInCurrentRoom.length === 1) {
Object.values(callsInCurrentRoom).forEach(call => {
const processedStream = processAudioVolume(stream, getters.microphoneInputLevel)
processedStream.getTracks().forEach(track => track.enabled = !getters.isMuted)
dispatch('_setOriginalStream', processedStream);
call.connection.getSenders()[0].replaceTrack(processedStream.getTracks()[0]);
//dispatch('_muteReconfigure', call);
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
});
} else {
await dispatch('_doConference', callsInCurrentRoom);
}
},
async setSpeaker({commit, getters, dispatch}, dId) {
if (!getters.getOutputDeviceList.find(({deviceId}) => deviceId === dId)) {
return
}
commit(STORE_MUTATION_TYPES.SET_SELECTED_OUTPUT_DEVICE, dId);
const activeCallList = Object.values(activeCalls);
if (activeCallList.length === 0) {
return;
}
const callsInCurrentRoom = activeCallList.filter(call => call.roomId === getters.getCurrentActiveRoomId);
if (callsInCurrentRoom.length === 1) {
activeCallList.forEach(call => {
call.audioTag.setSinkId(dId);
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
});
} else {
await dispatch('_doConference', callsInCurrentRoom);
}
},
async setCurrentActiveRoom({commit, getters, dispatch}, roomId) {
const oldRoomId = getters.getCurrentActiveRoomId;
if (roomId === oldRoomId) {
return;
}
commit(STORE_MUTATION_TYPES.SET_CURRENT_ACTIVE_ROOM_ID, roomId);
await dispatch('_roomReconfigure', oldRoomId)
await dispatch('_roomReconfigure', roomId)
},
setDND({commit}, value) {
commit(STORE_MUTATION_TYPES.SET_DND, value);
},
doCallHold({commit}, {callId, toHold, automatic}) {
const call = activeCalls[callId];
call._automaticHold = automatic || false;
if (toHold) {
call.hold();
} else {
call.unhold();
}
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
},
async _triggerAddStream({dispatch, getters, commit}, {event, call}) {
commit(STORE_MUTATION_TYPES.SET_MUTED, getters.muteWhenJoin);
const stream = await navigator.mediaDevices.getUserMedia(getters.getUserMediaConstraints);
const processedStream = processAudioVolume(stream, getters.microphoneInputLevel)
const muteMicro = getters.isMuted || getters.muteWhenJoin
processedStream.getTracks().forEach(track => track.enabled = !muteMicro)
dispatch('_setOriginalStream', processedStream);
await call.connection.getSenders()[0].replaceTrack(processedStream.getTracks()[0]);
//dispatch('_muteReconfigure', call);
syncStream(event, call, getters.getSelectedOutputDevice, getters.speakerVolume);
dispatch('_getCallQuality', call);
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
},
doCall({dispatch, getters, commit}, {target, addToCurrentRoom = false}) {
const activeRoomId = getters.getCurrentActiveRoomId
if (!getters._uaInit) {
return console.error('Run init action first');
}
if (target.toString().length === 0) {
return console.error('Target must be passed');
}
const call = UA.call(`sip:${target}@${getters.getSipDomain}`, getters.getSipOptions);
commit(STORE_MUTATION_TYPES.CALL_ADDING_IN_PROGRESS, call._id);
if (addToCurrentRoom && activeRoomId) {
dispatch('callChangeRoom', {callId: call._id, roomId: activeRoomId})
}
call.connection.addEventListener('addstream', async event => {
dispatch('_triggerAddStream', {event, call});
})
},
callTerminate(context, callId) {
const call = activeCalls[callId];
if (call._status !== 8) {
call.terminate();
}
},
callTransfer({commit, getters}, {callId, target}) {
if (target.toString().length === 0) {
return console.error('Target must be passed');
}
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId, isTransferring: true });
const call = activeCalls[callId];
call.refer(`sip:${target}@${getters.getSipDomain}`);
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
},
callMerge({commit}, roomId) {
const callsInRoom = Object.values(activeCalls).filter((call) => call.roomId === roomId)
if (callsInRoom.length !== 2) return
const firstCall = callsInRoom[0]
const secondCall = callsInRoom[1]
if (!firstCall || !secondCall) {
return
}
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId: firstCall._id, isMerging: true });
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId: secondCall._id, isMerging: true });
firstCall.refer(secondCall.remote_identity._uri.toString(), {'replaces': secondCall});
commit(STORE_MUTATION_TYPES.UPDATE_CALL, firstCall);
},
callMergeByIds({commit}, {firstCallId, secondCallId}) {
const firstCall = Object.values(activeCalls).find((call) => call._id === firstCallId)
const secondCall = Object.values(activeCalls).find((call) => call._id === secondCallId)
if (!firstCall || !secondCall) {
return
}
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId: firstCall._id, isMerging: true });
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId: secondCall._id, isMerging: true });
firstCall.refer(secondCall.remote_identity._uri.toString(), {'replaces': secondCall});
commit(STORE_MUTATION_TYPES.UPDATE_CALL, firstCall);
},
async callMove({dispatch, commit}, {callId, roomId}) {
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId, isMoving: true });
await dispatch('callChangeRoom', {callId, roomId})
commit(STORE_MUTATION_TYPES.UPDATE_CALL_STATUS, { callId, isMoving: false });
},
sendDTMF({dispatch, commit}, {callId, value}) {
const validation_regex = /^[A-D0-9\#\*]+$/g
if (!validation_regex.test(value)) {
throw new Error('Not allowed character in DTMF input')
}
const call = activeCalls[callId];
call.sendDTMF(value);
},
callAnswer({commit, getters, dispatch}, callId) {
const call = activeCalls[callId];
dispatch('_cancelAllOutgoingUnanswered');
call.answer(getters.getSipOptions);
commit(STORE_MUTATION_TYPES.UPDATE_CALL, call);
dispatch('setCurrentActiveRoom', call.roomId); //TODO: move to top
call.connection.addEventListener('addstream', async event => {
dispatch('_triggerAddStream', {event, call});
});
},
_getCallQuality({dispatch, commit, getters}, call) {
const metrics = new WebRTCMetrics(getters.metricConfig);
const probe = metrics.createProbe(call.connection, {
cid: call._id
});
let inboundKeys = []
let inboundAudio
probe.onreport = (probe) => {
//console.log('probe', probe)
const inboundMetrics = Object.entries(probe.audio).filter(([key, value]) => {
return value.direction === "inbound"
})
inboundMetrics.forEach(([key, value]) => {
if (!inboundKeys.includes(key)) {
inboundKeys.push(key)
inboundAudio = key
}
});
const inboundAudioMetric = probe.audio[inboundAudio]
const metrics = filterObjectkeys(inboundAudioMetric, METRIC_KEYS_TO_INCLUDE)
metrics.callId = call._id
commit(STORE_MUTATION_TYPES.SET_CALL_METRICS, metrics);
};
dispatch('subscribe', {type: CALL_EVENT_LISTENER_TYPE.CALL_ENDED, listener: (session) => {
if (session._id === call._id) {
metrics.stopAllProbes();
}
}})
metrics.startAllProbes();
},
async callChangeRoom({dispatch}, {callId, roomId}) {
const oldRoomId = activeCalls[callId].roomId;
activeCalls[callId].roomId = roomId;
await dispatch('setCurrentActiveRoom', roomId);
return Promise.all([
dispatch('_roomReconfigure', oldRoomId),
dispatch('_roomReconfigure', roomId)
]).then(() => {
dispatch('_deleteRoomIfEmpty', oldRoomId);
dispatch('_deleteRoomIfEmpty', roomId);
})
},
subscribe({commit}, value) {
commit(STORE_MUTATION_TYPES.ADD_LISTENER, value)
},
removeListener({commit}, type) {
commit(STORE_MUTATION_TYPES.REMOVE_LISTENER, type)
},
init({commit, dispatch, getters}, {configuration, socketInterfaces, listeners = [], sipDomain, sipOptions}) {
configuration.sockets = socketInterfaces.map(sock => new JsSIP.WebSocketInterface(sock))
UA = new JsSIP.UA(configuration);
UA.start();
listeners.push({
name: 'newRTCSession',
cb: ({session}) => {
if (getters.isDND) {
session.terminate({status_code: 486, reason_phrase: "Do Not Disturb"})
return
}
// stop timers on ended and failed
session._events.ended = function (event) {
dispatch('_triggerListener', {listenerType: CALL_EVENT_LISTENER_TYPE.CALL_ENDED, session, event});
dispatch('_activeCallListRemove', session);
dispatch('_stopCallTimer', session._id);
commit(STORE_MUTATION_TYPES.REMOVE_CALL_STATUS, session._id);
commit(STORE_MUTATION_TYPES.REMOVE_CALL_METRICS, session._id);
if (!Object.keys(activeCalls).length) {
commit(STORE_MUTATION_TYPES.SET_MUTED, false);
}
};
session._events.progress = function (event) {
dispatch('_triggerListener', {listenerType: CALL_EVENT_LISTENER_TYPE.CALL_PROGRESS, session, event});
};
session._events.failed = function (event) {
dispatch('_triggerListener', {listenerType: CALL_EVENT_LISTENER_TYPE.CALL_FAILED, session, event});
if (session._id === getters.callAddingInProgress) {
commit(STORE_MUTATION_TYPES.CALL_ADDING_IN_PROGRESS, null);
}
dispatch('_activeCallListRemove', session);
dispatch('_stopCallTimer', session._id);
commit(STORE_MUTATION_TYPES.REMOVE_CALL_STATUS, session._id);
commit(STORE_MUTATION_TYPES.REMOVE_CALL_METRICS, session._id);
if (!Object.keys(activeCalls).length) {
commit(STORE_MUTATION_TYPES.SET_MUTED, false);
}
};
session._events.confirmed = function (event) {
dispatch('_triggerListener', {listenerType: CALL_EVENT_LISTENER_TYPE.CALL_CONFIRMED, session, event});
commit(STORE_MUTATION_TYPES.UPDATE_CALL, session);
if (session._id === getters.callAddingInProgress) {
commit(STORE_MUTATION_TYPES.CALL_ADDING_IN_PROGRESS, null);
}
};
dispatch('_triggerListener', {listenerType: CALL_EVENT_LISTENER_TYPE.NEW_CALL, session});
dispatch('_addCall', session);
if (session.direction === CONSTRAINTS.CALL_DIRECTION_OUTGOING) {
dispatch('setCurrentActiveRoom', session.roomId);
}
}
});
listeners.forEach(({name, cb}) => UA.on(name, cb));
commit(STORE_MUTATION_TYPES.SET_SIP_DOMAIN, sipDomain);
commit(STORE_MUTATION_TYPES.SET_SIP_OPTIONS, sipOptions);
commit(STORE_MUTATION_TYPES.SET_UA_INIT);
},
setMetricConfig({commit}, config) {
commit(STORE_MUTATION_TYPES.SET_METRIC_CONFIG, config);
},
}
});
}
export default {
/**
* @param {Object} Vue
* @param {VSIPOptions} options
*/
install(Vue, options) {
initStoreModule(options);
},
STORAGE_KEYS,
STORE_MUTATION_TYPES,
CALL_EVENT_LISTENER_TYPE,
CONSTRAINTS,
CALL_KEYS_TO_INCLUDE
}