From 238f78cb05295392cf4603fcefe420bb28bdd95e Mon Sep 17 00:00:00 2001 From: Hans-Peter Herzog Date: Wed, 22 Sep 2021 13:56:02 +0200 Subject: [PATCH] TT#142700 CSC: Create a PoC integration of the VoIP(NGCP) call using Javascript library JsSip in combination with Kamailio's WebSocket module - Enable and disable camera during the call - Enable and disable screen during the call - Switch from camera to screen and back - Send in-band DTMF - Send "603 Decline" on termination Change-Id: Ife56ca49cadade44ee9b70b77b3f345b262be9d9 --- env/Dockerfile | 2 +- package.json | 1 + quasar.conf.js | 2 +- src/api/ngcp-call.js | 298 ++++++++++++++++++++++++ src/boot/ngcp-call.js | 67 ++++++ src/boot/rtc-engine.js | 76 ------ src/components/CscMainMenuTop.vue | 8 +- src/components/call/CscCall.vue | 70 +++--- src/layouts/CscLayoutMain.vue | 21 +- src/pages/CscPageHome.vue | 20 +- src/plugins/call.js | 327 -------------------------- src/plugins/conference.js | 163 ------------- src/plugins/rtc-engine.js | 189 --------------- src/rtc-engine-library.js | 54 +++++ src/store/call.js | 374 ------------------------------ src/store/call/actions.js | 73 ++++++ src/store/call/common.js | 29 +++ src/store/call/getters.js | 138 +++++++++++ src/store/call/index.js | 12 + src/store/call/mutations.js | 110 +++++++++ src/store/call/state.js | 23 ++ src/store/index.js | 2 +- src/store/pbx.js | 3 - src/store/user.js | 37 ++- t/Dockerfile | 2 +- yarn.lock | 37 ++- 26 files changed, 910 insertions(+), 1228 deletions(-) create mode 100644 src/api/ngcp-call.js create mode 100644 src/boot/ngcp-call.js delete mode 100644 src/boot/rtc-engine.js delete mode 100644 src/plugins/call.js delete mode 100644 src/plugins/conference.js delete mode 100644 src/plugins/rtc-engine.js create mode 100644 src/rtc-engine-library.js delete mode 100644 src/store/call.js create mode 100644 src/store/call/actions.js create mode 100644 src/store/call/common.js create mode 100644 src/store/call/getters.js create mode 100644 src/store/call/index.js create mode 100644 src/store/call/mutations.js create mode 100644 src/store/call/state.js diff --git a/env/Dockerfile b/env/Dockerfile index 717c5978..4b2bc204 100644 --- a/env/Dockerfile +++ b/env/Dockerfile @@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-buster:latest # is updated with the current date. It will force refresh of all # of the base images and things like `apt-get update` won't be using # old cached versions when the Dockerfile is built. -ENV REFRESHED_AT 2021-07-07 +ENV REFRESHED_AT 2021-09-27 # files that get-code generates COPY env/sources.list.d/builddeps.list /etc/apt/sources.list.d/ diff --git a/package.json b/package.json index b150cf8c..7dd69811 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "core-js": "^3.6.5", "file-saver": "^2.0.2", "jest-junit": "^11.1.0", + "jssip": "3.8.2", "jwt-decode": "^2.2.0", "load-script": "^1.0.0", "lodash": "4.17.21", diff --git a/quasar.conf.js b/quasar.conf.js index abe0bfa2..31a41c48 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -27,11 +27,11 @@ module.exports = function (ctx) { // https://quasar.dev/quasar-cli/boot-files boot: [ 'appConfig', - 'rtc-engine', 'filters', 'vuelidate', 'i18n', 'vue-resource', + 'ngcp-call', 'user', 'routes', 'vue-scrollto', diff --git a/src/api/ngcp-call.js b/src/api/ngcp-call.js new file mode 100644 index 00000000..2b562925 --- /dev/null +++ b/src/api/ngcp-call.js @@ -0,0 +1,298 @@ + +import { + EventEmitter +} from 'events' +import jssip from 'jssip' + +let $baseWebSocketUrl = null +let $subscriber = null +let $socket = null +let $userAgent = null +let $outgoingRtcSession = null +let $incomingRtcSession = null +let $localMediaStream = null +let $remoteMediaStream = null +let $isVideoScreen = false + +const TERMINATION_OPTIONS = { + status_code: 603, + reason_phrase: 'Decline' +} + +export const callEvent = new EventEmitter() + +function handleRemoteMediaStream (trackEvent) { + const stream = trackEvent.streams[0] + if (!$remoteMediaStream) { + $remoteMediaStream = stream + callEvent.emit('remoteStream', $remoteMediaStream) + } else if ($remoteMediaStream && $remoteMediaStream.id !== stream.id) { + $remoteMediaStream = stream + callEvent.emit('remoteStream', $remoteMediaStream) + } +} + +function getSubscriberUri () { + return 'sip:' + $subscriber.username + '@' + $subscriber.domain +} + +function callCreateSocket () { + return new jssip.WebSocketInterface($baseWebSocketUrl + '/' + $subscriber.username) +} + +export function callConfigure ({ baseWebSocketUrl }) { + $baseWebSocketUrl = baseWebSocketUrl +} + +export async function callInitialize ({ subscriber }) { + $subscriber = subscriber + callRegister() +} + +export function callRegister () { + if (!$socket) { + $socket = callCreateSocket() + const config = { + sockets: [$socket], + uri: getSubscriberUri(), + password: $subscriber.password + } + $userAgent = new jssip.UA(config) + const delegateEvent = (eventName) => { + $userAgent.on(eventName, (event) => callEvent.emit(eventName, event)) + } + delegateEvent('connected') + delegateEvent('disconnected') + delegateEvent('newRTCSession') + delegateEvent('newMessage') + delegateEvent('registered') + delegateEvent('unregistered') + delegateEvent('registrationFailed') + $userAgent.on('newRTCSession', (event) => { + if (event.originator === 'remote') { + $incomingRtcSession = event.session + $incomingRtcSession.on('peerconnection', () => { + $incomingRtcSession.connection.ontrack = handleRemoteMediaStream + }) + $incomingRtcSession.on('failed', (failedEvent) => { + callEvent.emit('incomingFailed', failedEvent) + }) + $incomingRtcSession.on('ended', (failedEvent) => { + callEvent.emit('incomingEnded', failedEvent) + $incomingRtcSession = null + }) + callEvent.emit('incoming', $incomingRtcSession) + } + }) + $userAgent.start() + } +} + +export function callUnregister () { + if ($userAgent) { + $userAgent.unregister({ + all: true + }) + } +} + +export async function callStart ({ number }) { + $localMediaStream = await callCreateLocalAudioStream() + callEvent.emit('localStream', $localMediaStream) + $outgoingRtcSession = $userAgent.call(number, { + eventHandlers: { + progress (event) { + callEvent.emit('outgoingProgress', event) + }, + failed (event) { + callEvent.emit('outgoingFailed', event) + }, + confirmed (event) { + callEvent.emit('outgoingConfirmed', event) + }, + ended (event) { + callEvent.emit('outgoingEnded', event) + $outgoingRtcSession = null + } + }, + mediaStream: $localMediaStream + }) + $outgoingRtcSession.connection.ontrack = handleRemoteMediaStream + const delegateEvent = (eventName, newName) => { + $outgoingRtcSession.on(eventName, (event) => callEvent.emit(newName, event)) + } + delegateEvent('failed', 'outgoingFailed') +} + +export async function callAccept () { + $localMediaStream = await callCreateLocalAudioStream() + callEvent.emit('localStream', $localMediaStream) + if ($incomingRtcSession) { + $incomingRtcSession.answer({ + mediaStream: $localMediaStream + }) + } +} + +export async function callCreateLocalAudioStream () { + return await navigator.mediaDevices.getUserMedia({ + video: false, + audio: true + }) +} + +export function callGetRemoteMediaStream () { + return $remoteMediaStream +} + +export function callGetRemoteMediaStreamId () { + return $remoteMediaStream?.id +} + +export function callGetLocalMediaStream () { + return $localMediaStream +} + +export function callGetLocalMediaStreamId () { + return $localMediaStream?.id +} + +export function callGetRtcSession () { + if ($outgoingRtcSession) { + return $outgoingRtcSession + } else if ($incomingRtcSession) { + return $incomingRtcSession + } +} + +export async function callChangeVideoStream (stream) { + if ($localMediaStream && callGetRtcSession()) { + callGetRtcSession().connection.getSenders().forEach( + sender => callGetRtcSession().connection.removeTrack(sender) + ) + $localMediaStream.getTracks().forEach(track => track.stop()) + $localMediaStream = stream + $localMediaStream.getTracks().forEach( + track => callGetRtcSession().connection.addTrack(track, $localMediaStream) + ) + await callRenegotiate() + } +} + +export async function callAddCamera () { + await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({ + video: { + width: { + ideal: 4096 + }, + height: { + ideal: 2160 + } + }, + audio: true + })) + $isVideoScreen = false +} + +export async function callAddScreen () { + $isVideoScreen = true + await callChangeVideoStream(await navigator.mediaDevices.getDisplayMedia({ + video: { + width: { + ideal: 4096 + }, + height: { + ideal: 2160 + } + }, + audio: true + })) +} + +export async function callRemoveVideo () { + $isVideoScreen = false + await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({ + video: false, + audio: true + })) +} + +export async function callRenegotiate () { + callGetRtcSession().renegotiate({ + useUpdate: false + }, () => { + callEvent.emit('renegotiationSucceeded') + }) +} + +export function callHasRemoteVideo () { + return $remoteMediaStream?.getVideoTracks?.()?.length > 0 +} + +export function callHasLocalVideo () { + return $localMediaStream?.getVideoTracks?.()?.length > 0 +} + +export function callHasLocalCamera () { + return callHasLocalVideo() && !$isVideoScreen +} + +export function callHasLocalScreen () { + return callHasLocalVideo() && $isVideoScreen +} + +export function callToggleMicrophone () { + const config = { + audio: true, + video: false + } + const rtcSession = callGetRtcSession() + const muted = rtcSession?.isMuted() + if (muted.audio) { + rtcSession.unmute(config) + } else { + rtcSession.mute(config) + } +} + +export function callIsMuted () { + return callGetRtcSession()?.isMuted()?.audio +} + +export function callSendDTMF (tone, transport = 'RFC2833') { + const rtcSession = callGetRtcSession() + if (rtcSession) { + rtcSession.sendDTMF(tone, { + transportType: transport + }) + } +} + +export function callToggleRemoteAudio () { + if ($remoteMediaStream && $remoteMediaStream.getAudioTracks()[0]) { + $remoteMediaStream.getAudioTracks()[0].enabled = !$remoteMediaStream?.getAudioTracks()[0]?.enabled + } +} + +export function callIsRemoteAudioMuted () { + return !$remoteMediaStream?.getAudioTracks()[0]?.enabled +} + +export function callEnd () { + if ($outgoingRtcSession && !$outgoingRtcSession.isEnded()) { + $outgoingRtcSession.terminate(TERMINATION_OPTIONS) + $outgoingRtcSession = null + } + if ($incomingRtcSession && !$incomingRtcSession.isEnded()) { + $incomingRtcSession.terminate(TERMINATION_OPTIONS) + $incomingRtcSession = null + } + if ($localMediaStream) { + $localMediaStream.getTracks().forEach(track => track.stop()) + $localMediaStream = null + } + if ($remoteMediaStream) { + $remoteMediaStream.getTracks().forEach(track => track.stop()) + $remoteMediaStream = null + } +} diff --git a/src/boot/ngcp-call.js b/src/boot/ngcp-call.js new file mode 100644 index 00000000..51321838 --- /dev/null +++ b/src/boot/ngcp-call.js @@ -0,0 +1,67 @@ + +import { + v4 +} from 'uuid' +import _ from 'lodash' +import { + callConfigure, + callEnd, + callEvent, + callHasLocalCamera, + callHasLocalScreen, + callHasRemoteVideo, + callIsMuted, + callIsRemoteAudioMuted +} from 'src/api/ngcp-call' +import { errorVisibilityTimeout } from 'src/store/call/common' + +export default async ({ Vue, app, store }) => { + callConfigure({ + baseWebSocketUrl: app.$appConfig.baseWsUrl + '/wss/sip' + }) + const callFailed = (event) => { + let cause = event.cause + const reason = event.message?.parseHeader?.('Reason') + if (reason?.text) { + cause = reason.text + } + if (event.originator !== 'local') { + callEnd() + store.commit('call/endCall', cause) + setTimeout(() => { + store.commit('call/inputNumber') + }, errorVisibilityTimeout) + } + } + callEvent.on('connected', () => { + store.commit('call/enableCall') + }) + callEvent.on('disconnected', () => { + store.commit('call/disableCall') + }) + callEvent.on('outgoingProgress', (event) => { + store.commit('call/startRinging') + }) + callEvent.on('outgoingFailed', callFailed) + callEvent.on('incomingFailed', callFailed) + callEvent.on('outgoingEnded', callFailed) + callEvent.on('incomingEnded', callFailed) + callEvent.on('incoming', (session) => { + store.commit('call/incomingCall', { + number: _.get(session, 'remote_identity.uri.user', 'Unknown') + }) + }) + callEvent.on('localStream', () => { + store.commit('call/toggleMicrophone', !callIsMuted()) + }) + callEvent.on('remoteStream', () => { + store.commit('call/establishCall', { + mediaStreamId: v4(), + isLocalAudioMuted: callIsMuted(), + hasLocalCamera: callHasLocalCamera(), + hasLocalScreen: callHasLocalScreen(), + hasRemoteVideo: callHasRemoteVideo(), + isRemoteAudioMuted: callIsRemoteAudioMuted() + }) + }) +} diff --git a/src/boot/rtc-engine.js b/src/boot/rtc-engine.js deleted file mode 100644 index 5e1675ff..00000000 --- a/src/boot/rtc-engine.js +++ /dev/null @@ -1,76 +0,0 @@ -import Vue from 'vue' -import getRtcEnginePlugin from 'src/plugins/rtc-engine' -import CallPlugin from 'src/plugins/call' -import ConferencePlugin from 'src/plugins/conference' -import { errorVisibilityTimeout } from 'src/store/call' - -export default ({ Vue, store, app }) => { - const rtcPluginConfig = { - cdkScriptUrl: app.$appConfig.baseHttpUrl + '/rtc/files/dist/cdk-prod.js', - webSocketUrl: app.$appConfig.baseWsUrl + '/rtc/api', - ngcpApiBaseUrl: app.$appConfig.baseHttpUrl - // ngcpApiJwt: ... // Note: this value will be set in userInit action, with value from "getJwt" function - } - const RtcEnginePlugin = getRtcEnginePlugin(rtcPluginConfig) - Vue.use(RtcEnginePlugin) - Vue.use(CallPlugin) - Vue.use(ConferencePlugin) - - rtcEngine(store) - call(store) - conference(store) -} - -function rtcEngine (store) { - Vue.$rtcEngine.onSipNetworkConnected(() => { - store.commit('call/enableCall') - }).onSipNetworkDisconnected(() => { - store.commit('call/disableCall') - }).onConferenceNetworkConnected(() => { - store.commit('conference/enableConferencing') - }).onConferenceNetworkDisconnected(() => { - store.commit('conference/disableConferencing') - }) -} - -function call (store) { - Vue.$call.onIncoming(() => { - store.commit('call/incomingCall', { - number: Vue.$call.getNumber() - }) - }).onRemoteMedia((remoteMediaStream) => { - store.commit('call/establishCall', remoteMediaStream) - }).onEnded((reason) => { - Vue.$call.end() - store.commit('call/endCall', reason) - setTimeout(() => { - store.commit('call/inputNumber') - }, errorVisibilityTimeout) - }) -} - -function conference (store) { - Vue.$conference.onConferenceParticipantJoined((participant) => { - store.commit('conference/participantJoined', participant) - participant.onMediaStream(() => { - store.commit('conference/removeRemoteMedia', participant.id) - store.commit('conference/addRemoteMedia', participant.id) - }).onMediaEnded(() => { - store.commit('conference/removeRemoteMedia', participant.id) - }) - }).onConferenceParticipantLeft((participant) => { - store.commit('conference/participantLeft', participant) - store.commit('conference/removeRemoteMedia', participant.id) - store.commit('conference/setSelectedParticipant', participant.id) - }).onConferenceEvent((event) => { - store.commit('conference/event', event) - }).onConferenceMessage((message) => { - store.commit('conference/message', message) - }).onConferenceFile((file) => { - store.commit('conference/file', file) - }).onLocalMediaStreamEnded(() => { - store.commit('conference/disposeLocalMedia') - }).onConferenceEnded(() => { - store.dispatch('conference/leave') - }) -} diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue index 66c6047a..bd061b20 100644 --- a/src/components/CscMainMenuTop.vue +++ b/src/components/CscMainMenuTop.vue @@ -68,13 +68,7 @@ export default { icon: 'call', label: this.callStateTitle, sublabel: this.callStateSubtitle, - visible: this.isRtcEngineUiVisible - }, - { - to: '/conference', - icon: 'videocam', - label: this.$t('Join conference'), - visible: this.isRtcEngineUiVisible + visible: true }, { to: '/user/conversations', diff --git a/src/components/call/CscCall.vue b/src/components/call/CscCall.vue index e24cc77c..f109b8f5 100644 --- a/src/components/call/CscCall.vue +++ b/src/components/call/CscCall.vue @@ -170,7 +170,7 @@ @click="toggleMicrophone()" /> + - - - - - + class="q-mr-sm" + round + size="large" + @click="startCall('audioOnly')" + />
@@ -275,6 +278,7 @@ export default { 'hasRemoteVideo', 'isMicrophoneEnabled', 'isCameraEnabled', + 'isScreenEnabled', 'isRemoteVolumeEnabled', 'isMaximized', 'isDialpadOpened', @@ -435,6 +439,12 @@ export default { 'forwardHome', 'fetchAuthToken' ]), + ...mapActions('call', [ + 'toggleCamera', + 'toggleScreen', + 'toggleMicrophone', + 'toggleRemoteAudio' + ]), layoutResized () { this.$refs.call.fitMedia() }, @@ -476,15 +486,6 @@ export default { endCall () { this.$store.dispatch('call/end') }, - toggleMicrophone () { - this.$store.dispatch('call/toggleMicrophone') - }, - toggleCamera () { - this.$store.dispatch('call/toggleCamera') - }, - toggleRemoteVolume () { - this.$store.dispatch('call/toggleRemoteVolume') - }, clickDialpad (value) { this.$store.dispatch('call/sendDTMF', value) }, diff --git a/src/pages/CscPageHome.vue b/src/pages/CscPageHome.vue index f5024bab..cd8721cd 100644 --- a/src/pages/CscPageHome.vue +++ b/src/pages/CscPageHome.vue @@ -6,19 +6,11 @@
- - {{ $t('You can neither make a call nor receive one, since the RTC:engine is not active. If you operate a C5 CE then first upgrade to a C5 PRO to be able to use the RTC:engine.') }} - { - this.events.emit('connected') - this.network = $network - this.network.onIncomingCall((remoteCall) => { - if (this.network !== null && this.currentCall === null) { - this.currentCall = remoteCall - this.currentCall.onEnded(() => { - this.events.emit('ended', this.currentCall.endedReason) - }).onRemoteMedia((remoteMediaStream) => { - this.remoteMedia = remoteMediaStream - this.events.emit('remoteMedia', remoteMediaStream) - }).onRemoteMediaEnded(() => { - this.events.emit('remoteMediaEnded') - }).onError((err) => { - console.debug(err) - this.end() - this.events.emit('ended', err.message) - }) - } - this.events.emit('incoming') - }) - }).onSipNetworkDisconnected(() => { - this.events.emit('disconnected') - }) - } - } - - isAvailable () { - return this.network !== null && this.network.isConnected - } - - hasRunningCall () { - return this.currentCall !== null - } - - createLocalMedia (localMedia) { - return new Promise((resolve, reject) => { - // eslint-disable-next-line no-undef - const localMediaBuilder = cdk.media.create() - if (localMedia === LocalMedia.audioOnly || localMedia === LocalMedia.audioVideo || - localMedia === LocalMedia.audioScreen) { - localMediaBuilder.enableMicrophone() - } - if (localMedia === LocalMedia.audioVideo || localMedia === LocalMedia.videoOnly) { - localMediaBuilder.enableCamera() - } else if (localMedia === LocalMedia.audioScreen || localMedia === LocalMedia.screenOnly) { - localMediaBuilder.enableScreen() - } - localMediaBuilder.build().then((localMediaStream) => { - this.localMedia = localMediaStream - resolve(this.localMedia) - }).catch((err) => { - reject(err) - }) - }) - } - - start (peer, localMediaStream) { - if (this.network !== null && this.currentCall === null) { - peer = peer.replace(/(\s|\+)/, '') - this.currentCall = this.network.call(peer, { - localMediaStream: localMediaStream - }) - this.currentCall.onEnded(() => { - this.events.emit('ended', this.currentCall.endedReason) - this.end() - }).onRemoteMedia((remoteMediaStream) => { - this.remoteMedia = remoteMediaStream - this.events.emit('remoteMedia', remoteMediaStream) - }).onRemoteMediaEnded(() => { - this.events.emit('remoteMediaEnded') - }).onAccepted(() => { - this.events.emit('accepted') - }).onPending(() => { - this.events.emit('pending') - }).onRingingStart(() => { - this.events.emit('ringingStart') - }).onRingingStop(() => { - this.events.emit('ringingStop') - }).onError((err) => { - console.debug(err) - this.end() - this.events.emit('ended', err.message) - }) - } else if (this.network !== null) { - throw new CallAlreadyExists() - } else { - throw new NetworkNotConnected(this.networkTag) // TODO: "this.networkTag" is not defined. We should get this variable from somewhere - } - } - - getNumber () { - if (this.currentCall !== null) { - return this.currentCall.peer - } else { - return null - } - } - - onIncoming (listener) { - this.events.on('incoming', listener) - return this - } - - onAccepted (listener) { - this.events.on('accepted', listener) - return this - } - - onPending (listener) { - this.events.on('pending', listener) - return this - } - - onRingingStart (listener) { - this.events.on('ringingStart', listener) - return this - } - - onRingingStop (listener) { - this.events.on('ringingStop', listener) - return this - } - - onRemoteMedia (listener) { - this.events.on('remoteMedia', listener) - return this - } - - onRemoteMediaEnded (listener) { - this.events.on('remoteMediaEnded', listener) - return this - } - - onEnded (listener) { - this.events.on('ended', listener) - return this - } - - onConnected (listener) { - this.events.on('connected', listener) - return this - } - - onDisconnected (listener) { - this.events.on('disconnected', listener) - return this - } - - accept (localMediaStream) { - if (this.currentCall !== null) { - this.currentCall.accept(localMediaStream).then(() => { - this.events.emit('locallyAccepted') - }).catch((err) => { - console.debug(err) - this.end() - this.events.emit('ended', err.message) - }) - } else { - throw new Error('Remote call does not exist') - } - } - - hangUp () { - this.end() - } - - end () { - if (this.currentCall !== null) { - this.currentCall.end() - this.currentCall = null - } - this.endMedia() - } - - endMedia () { - if (this.localMedia !== null) { - this.localMedia.stop() - this.localMedia = null - } - if (this.remoteMedia !== null) { - this.remoteMedia.stop() - this.remoteMedia = null - } - } - - disableAudio () { - if (this.currentCall !== null) { - this.currentCall.disableAudio() - } - } - - enableAudio () { - if (this.currentCall !== null) { - this.currentCall.enableAudio() - } - } - - disableVideo () { - if (this.currentCall !== null) { - this.currentCall.disableVideo() - } - } - - enableVideo () { - if (this.currentCall !== null) { - this.currentCall.enableVideo() - } - } - - sendDTMF (char) { - if (this.currentCall !== null) { - this.currentCall.sendDTMF(char) - } - } - - getCall () { - if (this.currentCall !== null) { - return this.currentCall - } else { - return null - } - } - - getLocalMediaId () { - if (this.localMedia !== null) { - return this.localMedia.getStream().id - } - return null - } - - getLocalMediaStream () { - if (this.localMedia !== null) { - return this.localMedia.getStream() - } - return null - } - - getRemoteMediaId () { - if (this.remoteMedia !== null) { - return this.remoteMedia.getStream().id - } - return null - } - - getRemoteMediaStream () { - if (this.remoteMedia !== null) { - return this.remoteMedia.getStream() - } - return null - } - - hasRemoteVideo () { - if (this.remoteMedia !== null) { - return this.remoteMedia.hasVideo() - } - return false - } - - hasLocalVideo () { - if (this.localMedia !== null) { - return this.localMedia.hasVideo() - } - return false - } - - static getInstance () { - if (rtcEngineCallInstance === null) { - rtcEngineCallInstance = new RtcEngineCall() - } - return rtcEngineCallInstance - } -} - -export default { - install (Vue) { - Vue.$call = RtcEngineCall.getInstance() - Vue.$call.setRtcEngine(Vue.$rtcEngine) - } -} diff --git a/src/plugins/conference.js b/src/plugins/conference.js deleted file mode 100644 index e440e330..00000000 --- a/src/plugins/conference.js +++ /dev/null @@ -1,163 +0,0 @@ - -import EventEmitter from 'events' - -let conferencePlugin = null - -export class ConferencePlugin { - constructor () { - this.events = new EventEmitter() - this.rtcEngine = null - this.conference = null - this.localMediaStream = null - } - - setRtcEngine (rtcEngine) { - if (this.rtcEngine === null) { - this.rtcEngine = rtcEngine - this.rtcEngine.onConferenceNetworkConnected((network) => { - this.events.emit('connected') - network - .onConferenceParticipantJoined((participant) => { - this.events.emit('participantJoined', participant) - }) - .onConferenceParticipantLeft((participant) => { - this.events.emit('participantLeft', participant) - }) - .onConferenceEvent((event) => { - this.events.emit('conferenceEvent', event) - }) - .onConferenceMessage((message) => { - this.events.emit('conferenceMessage', message) - }) - .onConferenceFile((file) => { - this.events.emit('conferenceFile', file) - }) - .onConferenceEnded(() => { - this.events.emit('conferenceEnded') - }) - }).onConferenceNetworkDisconnected(() => { - this.events.emit('disconnected') - }) - } - } - - getNetwork () { - return this.rtcEngine.getConferenceNetwork() - } - - async joinConference (options) { - options.localMediaStream = this.getLocalMediaStream() - this.conference = await this.getNetwork().joinConference(options) - return this.conference - } - - async changeConferenceMedia () { - await this.getNetwork().changeConferenceMedia({ - localMediaStream: this.getLocalMediaStream() - }) - } - - async leaveConference () { - await this.getNetwork().leaveConference() - } - - onConferenceParticipantJoined (listener) { - this.events.on('participantJoined', listener) - return this - } - - onConferenceParticipantLeft (listener) { - this.events.on('participantLeft', listener) - return this - } - - onConferenceEvent (listener) { - this.events.on('conferenceEvent', listener) - return this - } - - onConferenceMessage (listener) { - this.events.on('conferenceMessage', listener) - return this - } - - onConferenceFile (listener) { - this.events.on('conferenceFile', listener) - return this - } - - onConferenceEnded (listener) { - this.events.on('conferenceEnded', listener) - return this - } - - onError (listener) { - this.events.on('error', listener) - return this - } - - static getInstance () { - if (conferencePlugin === null) { - conferencePlugin = new ConferencePlugin() - } - return conferencePlugin - } - - setLocalMediaStream (localMediaStream) { - this.removeLocalMediaStream() - this.localMediaStream = localMediaStream - this.localMediaStream.onEnded(() => { - this.events.emit('localMediaStreamEnded', this.localMediaStream) - }) - } - - getLocalMediaStream () { - return this.localMediaStream - } - - onLocalMediaStreamEnded (listener) { - this.events.on('localMediaStreamEnded', listener) - return this - } - - hasLocalMediaStream () { - return this.localMediaStream !== null - } - - removeLocalMediaStream () { - if (this.hasLocalMediaStream()) { - this.getLocalMediaStream().stop() - } - this.localMediaStream = null - } - - getLocalMediaStreamNative () { - if (this.hasLocalMediaStream()) { - return this.getLocalMediaStream().getStream() - } - return null - } - - getLocalParticipant () { - if (this.conference !== null) { - return this.conference.getLocalParticipant() - } else { - return null - } - } - - getRemoteParticipant (id) { - if (this.conference !== null) { - return this.conference.getRemoteParticipant(id) - } else { - return null - } - } -} - -export default { - install (Vue) { - Vue.$conference = ConferencePlugin.getInstance() - Vue.$conference.setRtcEngine(Vue.$rtcEngine) - } -} diff --git a/src/plugins/rtc-engine.js b/src/plugins/rtc-engine.js deleted file mode 100644 index 7e86cc06..00000000 --- a/src/plugins/rtc-engine.js +++ /dev/null @@ -1,189 +0,0 @@ - -import loadScript from 'load-script' -import EventEmitter from 'events' - -const scriptId = 'cdk' - -let rtcEnginePlugin = null - -export class RtcEnginePlugin { - constructor ({ - cdkScriptUrl = null, - webSocketUrl = null, - ngcpApiBaseUrl = null, - ngcpApiJwt = null - }) { - this.cdkScriptUrl = cdkScriptUrl - this.webSocketUrl = webSocketUrl - this.script = null - /** - * - * @type {cdk.Client} - */ - this.client = null - this.sessionToken = null - this.ngcpApiJwt = ngcpApiJwt - this.ngcpApiBaseUrl = ngcpApiBaseUrl - this.events = new EventEmitter() - } - - createMedia () { - // eslint-disable-next-line no-undef - return cdk.media.create() - } - - initialize () { - return new Promise((resolve, reject) => { - Promise.resolve().then(() => { - return this.loadLibrary() - }).then(() => { - return this.createSession() - }).then(() => { - return this.connectClient() - }).then(() => { - resolve() - }).catch((err) => { - reject(err) - }) - }) - } - - setNgcpApiJwt (jwt) { - this.ngcpApiJwt = jwt - } - - setNgcpApiBaseUrl (baseUrl) { - this.ngcpApiBaseUrl = baseUrl - } - - loadLibrary () { - return new Promise((resolve, reject) => { - if (this.script === null) { - loadScript(this.cdkScriptUrl, { - attrs: { - id: scriptId - } - }, (err, script) => { - this.script = script - if (err) { - console.debug(err) - reject(new Error('Unable to load RTC:Engine client library')) - } else { - resolve() - } - }) - } else { - resolve() - } - }) - } - - createSession () { - return new Promise((resolve, reject) => { - if (this.ngcpApiJwt !== null && this.sessionToken === null) { - // eslint-disable-next-line no-undef - cdk.ngcp.setApiBaseUrl(this.ngcpApiBaseUrl) - // eslint-disable-next-line no-undef - cdk.ngcp.setApiJwt(this.ngcpApiJwt) - // eslint-disable-next-line no-undef - cdk.ngcp.createRTCEngineSession().then((sessionToken) => { - this.sessionToken = sessionToken - resolve() - }).catch((err) => { - console.error(err) - reject(new Error('Unable to create RTC:Engine session')) - }) - } else if (this.ngcpApiJwt !== null && this.sessionToken !== null) { - resolve() - } else { - throw new Error('Can not create RTC:Engine session without a valid NGCP API JWT') - } - }) - } - - connectClient () { - return new Promise((resolve, reject) => { - if (this.client === null) { - // eslint-disable-next-line no-undef - this.client = new cdk.Client({ - url: this.webSocketUrl, - userSession: this.sessionToken - }) - this.client.onConnect(() => { - this.events.emit('connected') - try { - const conferenceNetwork = this.client.getNetworkByTag('conference') - conferenceNetwork.onConnect(() => { - this.events.emit('conference-network-connected', conferenceNetwork) - }).onDisconnect(() => { - this.events.emit('conference-network-disconnected', conferenceNetwork) - }) - const sipNetwork = this.client.getNetworkByTag('sip') - sipNetwork.onConnect(() => { - this.events.emit('sip-network-connected', sipNetwork) - }).onDisconnect(() => { - this.events.emit('sip-network-disconnected', sipNetwork) - }) - } catch (e) { - reject(new Error('Unable to connect to a specific network by RTCEngine client')) - } - resolve() - }) - this.client.onDisconnect(() => { - reject(new Error('Unable to connect RTCEngine client')) - }) - } else { - resolve() - } - }) - } - - onSipNetworkConnected (listener) { - this.events.on('sip-network-connected', listener) - return this - } - - onSipNetworkDisconnected (listener) { - this.events.on('sip-network-disconnected', listener) - return this - } - - onConferenceNetworkConnected (listener) { - this.events.on('conference-network-connected', listener) - return this - } - - onConferenceNetworkDisconnected (listener) { - this.events.on('conference-network-disconnected', listener) - return this - } - - onConnected (listener) { - this.events.on('connected', listener) - return this - } - - onDisconnected (listener) { - this.events.on('disconnected', listener) - return this - } - - getConferenceNetwork () { - return this.client.getNetworkByTag('conference') - } - - static getInstance (rtcConfig = {}) { - if (rtcEnginePlugin === null) { - rtcEnginePlugin = new RtcEnginePlugin(rtcConfig) - } - return rtcEnginePlugin - } -} - -export default function getVuePlugin (rtcConfig) { - return { - install (Vue) { - Vue.$rtcEngine = RtcEnginePlugin.getInstance(rtcConfig) - } - } -} diff --git a/src/rtc-engine-library.js b/src/rtc-engine-library.js new file mode 100644 index 00000000..06cdc267 --- /dev/null +++ b/src/rtc-engine-library.js @@ -0,0 +1,54 @@ +import loadScript from 'load-script' + +const RTC_ENGINE_LIBRARY_ID = 'ngcp-rtc-engine-library' + +export const LocalMedia = { + audioOnly: 'audioOnly', + audioVideo: 'audioVideo', + videoOnly: 'videoOnly', + audioScreen: 'audioScreen', + screenOnly: 'screenOnly' +} + +export function loadRtcEngineLibrary ({ scriptUrl }) { + return new Promise((resolve, reject) => { + const script = document.getElementById(RTC_ENGINE_LIBRARY_ID) + if (!script) { + loadScript(scriptUrl, { + attrs: { + id: RTC_ENGINE_LIBRARY_ID + } + }, (err) => { + if (err) { + reject(new Error('Unable to load RTC:Engine client library')) + } else { + resolve() + } + }) + } else { + resolve() + } + }) +} + +export function unloadRtcEngineLibrary () { + const script = document.getElementById(RTC_ENGINE_LIBRARY_ID) + if (script) { + script.remove() + } +} + +export async function rtcEngineCreateMedia (localMedia) { + // eslint-disable-next-line no-undef + const localMediaBuilder = cdk.media.create() + if (localMedia === LocalMedia.audioOnly || localMedia === LocalMedia.audioVideo || + localMedia === LocalMedia.audioScreen) { + localMediaBuilder.enableMicrophone() + } + if (localMedia === LocalMedia.audioVideo || localMedia === LocalMedia.videoOnly) { + localMediaBuilder.enableCamera() + } else if (localMedia === LocalMedia.audioScreen || localMedia === LocalMedia.screenOnly) { + localMediaBuilder.enableScreen() + } + return await localMediaBuilder.build() +} diff --git a/src/store/call.js b/src/store/call.js deleted file mode 100644 index 2e4c949f..00000000 --- a/src/store/call.js +++ /dev/null @@ -1,374 +0,0 @@ - -import _ from 'lodash' -import Vue from 'vue' -import { - normalizeDestination -} from '../filters/number-format' -import { - startCase -} from '../filters/string' -import { - i18n -} from 'src/boot/i18n' - -export var CallState = { - input: 'input', - initiating: 'initiating', - ringing: 'ringing', - incoming: 'incoming', - established: 'established', - ended: 'ended' -} -export var CallStateTitle = { - get input () { return i18n.t('Start new call') }, - get initiating () { return i18n.t('Calling') }, - get ringing () { return i18n.t('Ringing at') }, - get incoming () { return i18n.t('Incoming call from') }, - get established () { return i18n.t('In call with') }, - get ended () { return i18n.t('Call ended') } -} - -export var MediaType = { - audioOnly: 'audioOnly', - audioVideo: 'audioVideo', - audioScreen: 'audioScreen' -} - -export const errorVisibilityTimeout = 5000 -export const reinitializeTimeout = 5000 - -function handleUserMediaError (context, err) { - const errName = _.get(err, 'name', null) - const errMessage = _.get(err, 'message', null) - if (errName === 'UserMediaError' && errMessage === 'Permission denied') { - context.commit('endCall', 'userMediaPermissionDenied') - } - if (errMessage === 'plugin not detected') { - context.commit('desktopSharingInstall') - context.commit('hangUpCall') - } else if (errMessage === 'PermissionDenied') { - context.commit('endCall', 'desktopSharingPermissionDenied') - } else { - context.commit('endCall', errName) - } -} - -export default { - namespaced: true, - state: { - callEnabled: false, - endedReason: null, - callState: CallState.input, - number: '', - numberInput: '', - localMediaStream: null, - remoteMediaStream: null, - caller: false, - callee: false, - desktopSharingInstall: false, - microphoneEnabled: true, - cameraEnabled: true, - remoteVolumeEnabled: true, - maximized: false, - dialpadOpened: false - }, - getters: { - isCallEnabled (state) { - return state.callEnabled - }, - endedReason (state) { - return state.endedReason - }, - callNumber (state) { - return state.number - }, - callNumberInput (state) { - return state.numberInput - }, - // isNetworkConnected(state) { - // return state.initializationState === RequestState.succeeded; - // }, - // isCallAvailable(state, getters) { - // return getters.isNetworkConnected; - // }, - // isCallInitializing(state, getters, rootState, rootGetters) { - // return state.initializationState === RequestState.requesting || - // rootGetters['user/userDataRequesting']; - // }, - // isCallInitialized(state) { - // return state.initializationState === RequestState.succeeded - // }, - // hasCallInitError(state) { - // return state.initializationError !== null; - // }, - // callInitError(state) { - // return state.initializationError; - // }, - isCallInitializing (state, getters, rootState, rootGetters) { - return rootGetters['user/isRtcEngineInitializing'] - }, - isPreparing (state) { - return state.callState === CallState.input - }, - isInitiating (state) { - return state.callState === CallState.initiating - }, - isIncoming (state) { - return state.callState === CallState.incoming - }, - isTrying (state) { - return state.callState === CallState.initiating || - state.callState === CallState.ringing - }, - isRinging (state) { - return state.callState === CallState.ringing - }, - isCalling (state) { - return state.callState === CallState.initiating || - state.callState === CallState.ringing || - state.callState === CallState.established || - state.callState === CallState.incoming || - state.callState === CallState.ended - }, - isEstablished (state) { - return state.callState === CallState.established - }, - isEnded (state) { - return state.callState === CallState.ended - }, - hasRtcEngineCapability (state, getters, rootState, rootGetters) { - return rootGetters['user/hasRtcEngineCapability'] - }, - hasRtcEngineCapabilityEnabled (state, getters, rootState, rootGetters) { - return rootGetters['user/hasRtcEngineCapabilityEnabled'] - }, - hasRemoteVideo (state) { - if (state.remoteMediaStream !== null) { - return Vue.$call.hasRemoteVideo() - } - }, - hasLocalVideo (state) { - if (state.localMediaStream !== null) { - return Vue.$call.hasLocalVideo() - } - }, - hasVideo (state, getters) { - return getters.hasLocalVideo || getters.hasRemoteVideo - }, - isAudioEnabled (state) { - return state.audioEnabled - }, - isVideoEnabled (state) { - return state.videoEnabled - }, - isCaller (state) { - return state.caller - }, - isCallee (state) { - return state.callee - }, - callState (state) { - return state.callState - }, - desktopSharingInstall (state) { - return state.desktopSharingInstall - }, - localMediaStream (state) { - if (state.localMediaStream !== null) { - return Vue.$call.getLocalMediaStream() - } - return null - }, - remoteMediaStream (state) { - if (state.remoteMediaStream !== null) { - return Vue.$call.getRemoteMediaStream() - } - return null - }, - isMicrophoneEnabled (state) { - return state.microphoneEnabled - }, - isCameraEnabled (state) { - return state.cameraEnabled - }, - isRemoteVolumeEnabled (state) { - return state.remoteVolumeEnabled - }, - isMaximized (state) { - return state.maximized - }, - isDialpadOpened (state) { - return state.dialpadOpened - }, - callNumberFormatted (state, getters) { - return normalizeDestination(getters.callNumber) - }, - callEndedReasonFormatted (state, getters) { - return startCase(getters.endedReason) - }, - callStateTitle (state) { - return CallStateTitle[state.callState] - }, - callStateSubtitle (state, getters) { - if (state.callState === CallState.initiating || - state.callState === CallState.ringing || - state.callState === CallState.incoming || - state.callState === CallState.established) { - return getters.callNumberFormatted - } else if (state.callState === CallState.ended) { - return getters.callEndedReasonFormatted - } else { - return '' - } - } - }, - mutations: { - numberInputChanged (state, numberInput) { - state.numberInput = numberInput - }, - inputNumber (state) { - state.callState = CallState.input - state.number = '' - state.numberInput = '' - state.endedReason = null - }, - startCalling (state, number) { - state.number = number - state.callState = CallState.initiating - state.caller = true - state.callee = false - state.endedReason = null - }, - localMediaSuccess (state) { - state.localMediaStream = Vue.$call.getLocalMediaId() - }, - startRinging (state) { - state.callState = CallState.ringing - }, - stopRinging (state) { - state.callState = CallState.established - }, - establishCall (state) { - state.remoteMediaStream = Vue.$call.getRemoteMediaId() - state.callState = CallState.established - state.microphoneEnabled = true - state.cameraEnabled = true - state.remoteVolumeEnabled = true - }, - incomingCall (state, options) { - state.callState = CallState.incoming - state.number = options.number - state.callee = true - state.caller = false - state.endedReason = null - }, - hangUpCall (state) { - state.callState = CallState.input - state.number = '' - state.numberInput = '' - state.endedReason = null - Vue.$call.hangUp() - }, - endCall (state, reason) { - if (state.endedReason === null) { - state.callState = CallState.ended - state.endedReason = reason - } - Vue.$call.end() - }, - sendDTMF (state, value) { - state.dtmf = value - }, - toggleMicrophone (state) { - state.microphoneEnabled = !state.microphoneEnabled - }, - toggleCamera (state) { - state.cameraEnabled = !state.cameraEnabled - }, - toggleRemoteVolume (state) { - state.remoteVolumeEnabled = !state.remoteVolumeEnabled - }, - maximize (state) { - state.dialpadOpened = false - state.maximized = true - }, - minimize (state) { - state.dialpadOpened = false - state.maximized = false - }, - toggleDialpad (state) { - state.dialpadOpened = !state.dialpadOpened - }, - enableCall (state) { - state.callEnabled = true - }, - disableCall (state) { - state.callEnabled = false - } - }, - actions: { - start (context, localMedia) { - const number = context.getters.callNumberInput.replaceAll('(', '') - .replaceAll(')', '') - .replaceAll(' ', '') - .replaceAll('-', '') - context.commit('startCalling', number) - Promise.resolve().then(() => { - return Vue.$call.createLocalMedia(localMedia) - }).then((localMediaStream) => { - context.commit('localMediaSuccess') - Vue.$call.onRingingStart(() => { - context.commit('startRinging') - }).onRingingStop(() => { - context.commit('stopRinging') - }).start(number, localMediaStream) - }).catch((err) => { - Vue.$call.end() - handleUserMediaError(context, err) - setTimeout(() => { - context.commit('inputNumber') - }, errorVisibilityTimeout) - }) - }, - accept (context, localMedia) { - Vue.$call.createLocalMedia(localMedia).then((localMediaStream) => { - Vue.$call.accept(localMediaStream) - context.commit('localMediaSuccess') - }).catch((err) => { - Vue.$call.end() - handleUserMediaError(context, err) - setTimeout(() => { - context.commit('inputNumber') - }, errorVisibilityTimeout) - }) - }, - end (context) { - Vue.$call.end() - context.commit('hangUpCall') - }, - sendDTMF (context, value) { - if (Vue.$call.hasRunningCall()) { - Vue.$call.sendDTMF(value) - } - }, - toggleMicrophone (context) { - if (context.getters.isMicrophoneEnabled) { - Vue.$call.disableAudio() - } else { - Vue.$call.enableAudio() - } - context.commit('toggleMicrophone') - }, - toggleCamera (context) { - if (context.getters.isCameraEnabled) { - Vue.$call.disableVideo() - } else { - Vue.$call.enableVideo() - } - context.commit('toggleCamera') - }, - toggleRemoteVolume (context) { - context.commit('toggleRemoteVolume') - } - } -} diff --git a/src/store/call/actions.js b/src/store/call/actions.js new file mode 100644 index 00000000..d1a4aab1 --- /dev/null +++ b/src/store/call/actions.js @@ -0,0 +1,73 @@ +import { + callGetLocalMediaStreamId, + callAccept, + callEnd, + callStart, + callAddCamera, + callAddScreen, + callRemoveVideo, + callHasLocalVideo, + callToggleMicrophone, + callIsMuted, + callSendDTMF, + callToggleRemoteAudio, + callIsRemoteAudioMuted, callHasLocalScreen, callHasLocalCamera +} from 'src/api/ngcp-call' + +export default { + async start (context, localMedia) { + const number = context.getters.callNumberInput.replaceAll('(', '') + .replaceAll(')', '') + .replaceAll(' ', '') + .replaceAll('-', '') + context.commit('startCalling', number) + await callStart({ + number, + localMedia + }) + context.commit('localMediaSuccess', callGetLocalMediaStreamId()) + }, + async accept (context, localMedia) { + await callAccept({ + localMedia + }) + context.commit('localMediaSuccess', callGetLocalMediaStreamId()) + }, + async toggleMicrophone (context) { + callToggleMicrophone() + context.commit('toggleMicrophone', !callIsMuted()) + }, + async toggleCamera (context) { + if (!callHasLocalVideo() || callHasLocalScreen()) { + await callAddCamera() + context.commit('disableVideo') + context.commit('enableCamera') + } else { + await callRemoveVideo() + context.commit('disableVideo') + } + context.commit('localMediaSuccess', callGetLocalMediaStreamId()) + }, + async toggleScreen (context) { + if (!callHasLocalVideo() || callHasLocalCamera()) { + await callAddScreen() + context.commit('disableVideo') + context.commit('enableScreen') + } else { + await callRemoveVideo() + context.commit('disableVideo') + } + context.commit('localMediaSuccess', callGetLocalMediaStreamId()) + }, + end (context) { + callEnd() + context.commit('hangUpCall') + }, + sendDTMF (context, tone) { + callSendDTMF(tone) + }, + toggleRemoteAudio (context) { + callToggleRemoteAudio() + context.commit('toggleRemoteAudio', !callIsRemoteAudioMuted()) + } +} diff --git a/src/store/call/common.js b/src/store/call/common.js new file mode 100644 index 00000000..415b10ff --- /dev/null +++ b/src/store/call/common.js @@ -0,0 +1,29 @@ +import { + i18n +} from 'boot/i18n' + +export const CallState = { + input: 'input', + initiating: 'initiating', + ringing: 'ringing', + incoming: 'incoming', + established: 'established', + ended: 'ended' +} +export const CallStateTitle = { + get input () { return i18n.t('Start new call') }, + get initiating () { return i18n.t('Calling') }, + get ringing () { return i18n.t('Ringing at') }, + get incoming () { return i18n.t('Incoming call from') }, + get established () { return i18n.t('In call with') }, + get ended () { return i18n.t('Call ended') } +} + +export const MediaType = { + audioOnly: 'audioOnly', + audioVideo: 'audioVideo', + audioScreen: 'audioScreen' +} + +export const errorVisibilityTimeout = 5000 +export const reinitializeTimeout = 5000 diff --git a/src/store/call/getters.js b/src/store/call/getters.js new file mode 100644 index 00000000..e95843d4 --- /dev/null +++ b/src/store/call/getters.js @@ -0,0 +1,138 @@ +import { + callGetLocalMediaStream, + callGetRemoteMediaStream, + callHasRemoteVideo +} from 'src/api/ngcp-call' +import { + normalizeDestination +} from 'src/filters/number-format' +import { + startCase +} from 'src/filters/string' +import { + CallState, + CallStateTitle +} from 'src/store/call/common' + +export default { + isCallEnabled (state) { + return state.callEnabled + }, + endedReason (state) { + return state.endedReason + }, + callNumber (state) { + return state.number + }, + callNumberInput (state) { + return state.numberInput + }, + isPreparing (state) { + return state.callState === CallState.input + }, + isInitiating (state) { + return state.callState === CallState.initiating + }, + isIncoming (state) { + return state.callState === CallState.incoming + }, + isTrying (state) { + return state.callState === CallState.initiating || + state.callState === CallState.ringing + }, + isRinging (state) { + return state.callState === CallState.ringing + }, + isCalling (state) { + return state.callState === CallState.initiating || + state.callState === CallState.ringing || + state.callState === CallState.established || + state.callState === CallState.incoming || + state.callState === CallState.ended + }, + isEstablished (state) { + return state.callState === CallState.established + }, + isEnded (state) { + return state.callState === CallState.ended + }, + hasRtcEngineCapability (state, getters, rootState, rootGetters) { + return rootGetters['user/hasRtcEngineCapability'] + }, + hasRtcEngineCapabilityEnabled (state, getters, rootState, rootGetters) { + return rootGetters['user/hasRtcEngineCapabilityEnabled'] + }, + hasRemoteVideo (state) { + if (state.remoteMediaStream !== null) { + return callHasRemoteVideo() + } + }, + hasLocalVideo (state, getters) { + if (state.localMediaStream !== null) { + return getters.isScreenEnabled || getters.isCameraEnabled + } + }, + hasVideo (state, getters) { + return getters.hasLocalVideo || getters.hasRemoteVideo + }, + isCaller (state) { + return state.caller + }, + isCallee (state) { + return state.callee + }, + callState (state) { + return state.callState + }, + localMediaStream (state) { + if (state.localMediaStream) { + return callGetLocalMediaStream() + } + return null + }, + remoteMediaStream (state) { + if (state.remoteMediaStream) { + return callGetRemoteMediaStream() + } + return null + }, + isMicrophoneEnabled (state) { + return state.microphoneEnabled + }, + isCameraEnabled (state) { + return state.cameraEnabled + }, + isScreenEnabled (state) { + return state.screenEnabled + }, + isRemoteVolumeEnabled (state) { + return state.remoteAudioEnabled + }, + isMaximized (state) { + return state.maximized + }, + isDialpadOpened (state) { + return state.dialpadOpened + }, + callNumberFormatted (state, getters) { + return normalizeDestination(getters.callNumber) + }, + callEndedReasonFormatted (state, getters) { + return startCase(getters.endedReason) + }, + callStateTitle (state) { + return CallStateTitle[state.callState] + }, + callStateSubtitle (state, getters) { + if (state.callState === CallState.initiating || + state.callState === CallState.ringing || + state.callState === CallState.incoming || + state.callState === CallState.established) { + return getters.callNumberFormatted + } else if (state.callState === CallState.ended) { + return getters.callEndedReasonFormatted + } else { + return '' + } + } +} diff --git a/src/store/call/index.js b/src/store/call/index.js new file mode 100644 index 00000000..b865754c --- /dev/null +++ b/src/store/call/index.js @@ -0,0 +1,12 @@ +import state from './state' +import getters from './getters' +import mutations from './mutations' +import actions from './actions' + +export default { + namespaced: true, + state: state, + getters: getters, + mutations: mutations, + actions: actions +} diff --git a/src/store/call/mutations.js b/src/store/call/mutations.js new file mode 100644 index 00000000..d2fd9875 --- /dev/null +++ b/src/store/call/mutations.js @@ -0,0 +1,110 @@ +import { + CallState +} from 'src/store/call/common' + +export default { + numberInputChanged (state, numberInput) { + state.numberInput = numberInput + }, + inputNumber (state) { + state.callState = CallState.input + state.number = '' + state.numberInput = '' + state.endedReason = null + }, + startCalling (state, number) { + state.number = number + state.callState = CallState.initiating + state.caller = true + state.callee = false + state.endedReason = null + }, + localMediaSuccess (state, localMediaStreamId) { + state.localMediaStream = localMediaStreamId + }, + startRinging (state) { + state.callState = CallState.ringing + }, + stopRinging (state) { + state.callState = CallState.established + }, + establishCall (state, { + mediaStreamId, + isLocalAudioMuted = false, + hasLocalCamera = false, + hasLocalScreen = false, + hasRemoteVideo = false, + isRemoteAudioMuted = false + }) { + state.remoteMediaStream = mediaStreamId + state.callState = CallState.established + state.microphoneEnabled = !isLocalAudioMuted + state.cameraEnabled = hasLocalCamera + state.screenEnabled = hasLocalScreen + state.remoteAudioEnabled = !isRemoteAudioMuted + state.remoteVideoEnabled = hasRemoteVideo + }, + incomingCall (state, options) { + state.callState = CallState.incoming + state.number = options.number + state.callee = true + state.caller = false + state.endedReason = null + }, + hangUpCall (state) { + state.callState = CallState.input + state.number = '' + state.numberInput = '' + state.endedReason = null + }, + endCall (state, reason) { + if (state.endedReason === null) { + state.callState = CallState.ended + state.endedReason = reason + state.localMediaStream = null + state.remoteMediaStream = null + } + }, + sendDTMF (state, value) { + state.dtmf = value + }, + toggleMicrophone (state, enabled) { + state.microphoneEnabled = enabled + }, + setCamera (state, enabled) { + state.cameraEnabled = enabled + }, + enableCamera (state) { + state.cameraEnabled = true + }, + enableScreen (state) { + state.screenEnabled = true + }, + setScreen (state, enabled) { + state.screenEnabled = enabled + }, + disableVideo (state) { + state.cameraEnabled = false + state.screenEnabled = false + }, + toggleRemoteAudio (state, enabled) { + state.remoteAudioEnabled = enabled + }, + maximize (state) { + state.dialpadOpened = false + state.maximized = true + }, + minimize (state) { + state.dialpadOpened = false + state.maximized = false + }, + toggleDialpad (state) { + state.dialpadOpened = !state.dialpadOpened + }, + enableCall (state) { + state.callEnabled = true + }, + disableCall (state) { + state.callEnabled = false + } +} diff --git a/src/store/call/state.js b/src/store/call/state.js new file mode 100644 index 00000000..ee9071d2 --- /dev/null +++ b/src/store/call/state.js @@ -0,0 +1,23 @@ + +import { + CallState +} from 'src/store/call/common' + +export default { + callEnabled: false, + endedReason: null, + callState: CallState.input, + number: '', + numberInput: '', + localMediaStream: null, + remoteMediaStream: null, + caller: false, + callee: false, + microphoneEnabled: true, + cameraEnabled: false, + screenEnabled: false, + remoteAudioEnabled: true, + remoteVideoEnabled: true, + maximized: false, + dialpadOpened: false +} diff --git a/src/store/index.js b/src/store/index.js index 7644ef82..53316914 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,7 +5,7 @@ import { date } from 'quasar' import CallBlockingModule from './call-blocking' import CallForwardingModule from './call-forwarding' -import CallModule from './call' +import CallModule from 'src/store/call' import CallRecordingsModule from './call-recordings' import CallSettingsModule from './call-settings' import ConversationsModule from './conversations' diff --git a/src/store/pbx.js b/src/store/pbx.js index 3332ca18..4a04351a 100644 --- a/src/store/pbx.js +++ b/src/store/pbx.js @@ -11,9 +11,6 @@ import { import { RequestState } from './common' -// import { -// loadDeviceModel -// } from '../api/pbx-devices' import { getNumbers } from '../api/user' import { i18n diff --git a/src/store/user.js b/src/store/user.js index 050fefa3..930c27a0 100644 --- a/src/store/user.js +++ b/src/store/user.js @@ -1,6 +1,5 @@ 'use strict' -import Vue from 'vue' import _ from 'lodash' import { RequestState @@ -25,6 +24,7 @@ import { qrPayload } from 'src/helpers/qr' import { date } from 'quasar' +import { callInitialize } from 'src/api/ngcp-call' import { setLocal } from 'src/storage' export default { @@ -148,12 +148,6 @@ export default { return null } }, - isRtcEngineInitialized (state) { - return state.rtcEngineInitState === RequestState.succeeded - }, - isRtcEngineInitializing (state) { - return state.rtcEngineInitState === RequestState.requesting - }, getSubscriber (state) { return state.subscriber }, @@ -309,19 +303,20 @@ export default { } }, actions: { - login (context, options) { + async login (context, options) { context.commit('loginRequesting') - login(options.username, options.password).then((result) => { + try { + const result = await login(options.username, options.password) setJwt(result.jwt) setSubscriberId(result.subscriberId) context.commit('loginSucceeded', { jwt: getJwt(), subscriberId: getSubscriberId() }) - context.dispatch('initUser') - }).catch((err) => { + await context.dispatch('initUser') + } catch (err) { context.commit('loginFailed', err.message) - }) + } }, logout () { deleteJwt() @@ -339,21 +334,17 @@ export default { context.dispatch('logout') }, context.getters.jwtTTL * 1000) } - if (context.getters.hasRtcEngineCapabilityEnabled && context.getters.isRtcEngineUiVisible) { - context.commit('rtcEngineInitRequesting') - Vue.$rtcEngine.setNgcpApiJwt(getJwt()) - try { - await Vue.$rtcEngine.initialize() - context.commit('rtcEngineInitSucceeded') - } catch (err) { - console.debug(err) - context.commit('rtcEngineInitFailed', err.message) - } - } if (userData.subscriber.profile_id) { const profile = await getSubscriberProfile(userData.subscriber.profile_id) context.commit('setProfile', profile) } + try { + await callInitialize({ + subscriber: userData.subscriber + }) + } catch (err) { + console.log(err) + } await context.dispatch('forwardHome') } catch (err) { console.debug(err) diff --git a/t/Dockerfile b/t/Dockerfile index 4ea2d2ba..12246a11 100644 --- a/t/Dockerfile +++ b/t/Dockerfile @@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-bullseye:latest # is updated with the current date. It will force refresh of all # of the base images and things like `apt-get update` won't be using # old cached versions when the Dockerfile is built. -ENV REFRESHED_AT 2021-07-08 +ENV REFRESHED_AT 2021-09-27 ENV DEBIAN_FRONTEND noninteractive ENV DISPLAY=:0 diff --git a/yarn.lock b/yarn.lock index da071315..23f03f17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1427,6 +1427,13 @@ resolved "https://npm-registry.sipwise.com/@types/cordova/-/cordova-0.0.34.tgz#ea7addf74ecec3d7629827a0c39e2c9addc73d04" integrity sha1-6nrd907Ow9dimCegw54smt3HPQQ= +"@types/debug@^4.1.5": + version "4.1.7" + resolved "https://npm-registry.sipwise.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/electron-packager@14.0.0": version "14.0.0" resolved "https://npm-registry.sipwise.com/@types/electron-packager/-/electron-packager-14.0.0.tgz#f6dab1542fe02a3dd235d9bf66c8cb365f123902" @@ -1543,11 +1550,21 @@ resolved "https://npm-registry.sipwise.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== +"@types/ms@*": + version "0.7.31" + resolved "https://npm-registry.sipwise.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*": version "16.10.2" resolved "https://npm-registry.sipwise.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e" integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ== +"@types/node@^14.14.34": + version "14.17.21" + resolved "https://npm-registry.sipwise.com/@types/node/-/node-14.17.21.tgz#6359d8cf73481e312a43886fa50afc70ce5592c6" + integrity sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA== + "@types/q@^1.5.1": version "1.5.5" resolved "https://npm-registry.sipwise.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" @@ -4013,7 +4030,7 @@ debug@^4.0.1: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://npm-registry.sipwise.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -4880,7 +4897,7 @@ eventemitter3@^4.0.0: resolved "https://npm-registry.sipwise.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0: +events@^3.0.0, events@^3.3.0: version "3.3.0" resolved "https://npm-registry.sipwise.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -7457,6 +7474,17 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jssip@3.8.2: + version "3.8.2" + resolved "https://npm-registry.sipwise.com/jssip/-/jssip-3.8.2.tgz#5701ee6cbd4a728676574c9e3a914b9e8d2b7a34" + integrity sha512-YmRIMBkzilZZChAgFY2ksesQmMmJHSSATe8e2kQnFtMLdzmE3a1FhhcnzxxKzj+L2Xn6HOaXB6zczRaZMuXvqg== + dependencies: + "@types/debug" "^4.1.5" + "@types/node" "^14.14.34" + debug "^4.3.1" + events "^3.3.0" + sdp-transform "^2.14.1" + jwt-decode@^2.2.0: version "2.2.0" resolved "https://npm-registry.sipwise.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -10384,6 +10412,11 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +sdp-transform@^2.14.1: + version "2.14.1" + resolved "https://npm-registry.sipwise.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827" + integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw== + seek-bzip@^1.0.5: version "1.0.6" resolved "https://npm-registry.sipwise.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4"