From f1f5bc3583ae47db19845170c599b6ded42a48e2 Mon Sep 17 00:00:00 2001 From: Hans-Peter Herzog Date: Tue, 19 Mar 2019 09:05:09 +0100 Subject: [PATCH] TT#31302 Conferencing: As a Customer, I want to join a conference by using a URL Change-Id: I0186ae80529dfaf35d08a22deb4f9b249bc16279 --- dev-config.sh | 1 + src/components/CscMedia.vue | 8 +- src/components/layouts/Conference.vue | 205 ++++++++++++++ src/components/layouts/Default.vue | 12 +- .../pages/Conference/CscConferenceJoin.vue | 95 +++++++ .../pages/Conference/CscConferenceJoined.vue | 15 + .../pages/Conversations/Conversations.vue | 4 +- src/components/pages/Home.vue | 6 +- src/locales/en.json | 4 + src/main.js | 4 - src/plugins/call.js | 47 ++-- src/plugins/conference.js | 47 ++++ src/plugins/rtc-engine.js | 167 ++++++++++++ src/routes.js | 23 +- src/store/call.js | 111 +++----- src/store/conference.js | 258 ++++++++++++++++++ src/store/index.js | 44 ++- src/store/user.js | 39 ++- src/themes/app.common.styl | 13 + 19 files changed, 988 insertions(+), 115 deletions(-) create mode 100644 src/components/layouts/Conference.vue create mode 100644 src/components/pages/Conference/CscConferenceJoin.vue create mode 100644 src/components/pages/Conference/CscConferenceJoined.vue create mode 100644 src/plugins/conference.js create mode 100644 src/plugins/rtc-engine.js create mode 100644 src/store/conference.js diff --git a/dev-config.sh b/dev-config.sh index 22e2a138..b6ba6611 100755 --- a/dev-config.sh +++ b/dev-config.sh @@ -6,6 +6,7 @@ case "$1" in ngcpcfg set /etc/ngcp-config/config.yml www_admin.http_csc.csc_js_enable=yes ngcpcfg set /etc/ngcp-config/config.yml rtcengine.enable=yes ngcpcfg set /etc/ngcp-config/config.yml rtcengine.conference.enable=yes + ngcpcfg set /etc/ngcp-config/config.yml rtcengine.conference.type=janus ngcpcfg set /etc/ngcp-config/config.yml janus.enable=yes ngcpcfg set /etc/ngcp-config/config.yml fileshare.enable=yes ngcpcfg set /etc/ngcp-config/config.yml pbx.enable=yes diff --git a/src/components/CscMedia.vue b/src/components/CscMedia.vue index 46da6696..f48b83e6 100644 --- a/src/components/CscMedia.vue +++ b/src/components/CscMedia.vue @@ -86,7 +86,9 @@ }, 100); }, fitMediaToParent() { - if(typeof(this.$refs.media.videoWidth) === 'number' && + if(this.$refs.media && this.$refs.media && + this.$refs.media.videoWidth && this.$refs.media.videoHeight && + typeof(this.$refs.media.videoWidth) === 'number' && typeof(this.$refs.media.videoHeight) === 'number') { let parentAspectRatio = this.$parent.$el.clientWidth / this.$parent.$el.clientHeight; let isParentLandscape = parentAspectRatio >= 1; @@ -139,7 +141,9 @@ } }, fitMediaHeightToParent() { - if(typeof(this.$refs.media.videoWidth) === 'number' && + if(this.$refs.media && this.$refs.media && + this.$refs.media.videoWidth && this.$refs.media.videoHeight && + typeof(this.$refs.media.videoWidth) === 'number' && typeof(this.$refs.media.videoHeight) === 'number') { let videoAspectRatio = this.$refs.media.videoWidth / this.$refs.media.videoHeight; this.mediaWidth = this.width; diff --git a/src/components/layouts/Conference.vue b/src/components/layouts/Conference.vue new file mode 100644 index 00000000..de30aa06 --- /dev/null +++ b/src/components/layouts/Conference.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/src/components/layouts/Default.vue b/src/components/layouts/Default.vue index 949ed2cc..0a56e90e 100644 --- a/src/components/layouts/Default.vue +++ b/src/components/layouts/Default.vue @@ -245,6 +245,7 @@ 'title' ]), ...mapGetters('call', [ + 'isCallEnabled', 'callState', 'callNumber', 'callNumberInput', @@ -252,8 +253,6 @@ 'isCalling', 'localMediaStream', 'remoteMediaStream', - 'isCallAvailable', - 'hasCallInitError', 'hasVideo', 'hasLocalVideo', 'hasRemoteVideo', @@ -265,6 +264,9 @@ 'callStateTitle', 'callStateSubtitle' ]), + ...mapGetters('conference', [ + 'isConferencingEnabled' + ]), ...mapGetters('user', [ 'isLogged', 'hasUser', @@ -440,14 +442,14 @@ enableIncomingCallNotifications(); } }, - isCallAvailable(value) { + isCallEnabled(value) { if(value) { showToast(this.$i18n.t('toasts.callAvailable')); } }, - hasCallInitError(value) { + isConferencingEnabled(value) { if(value) { - showToast(this.$i18n.t('toasts.callNotAvailable')); + // showToast(this.$i18n.t('toasts.conferencingAvailable')); } }, createFaxState(state) { diff --git a/src/components/pages/Conference/CscConferenceJoin.vue b/src/components/pages/Conference/CscConferenceJoin.vue new file mode 100644 index 00000000..599d51d8 --- /dev/null +++ b/src/components/pages/Conference/CscConferenceJoin.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/pages/Conference/CscConferenceJoined.vue b/src/components/pages/Conference/CscConferenceJoined.vue new file mode 100644 index 00000000..64325f8e --- /dev/null +++ b/src/components/pages/Conference/CscConferenceJoined.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/components/pages/Conversations/Conversations.vue b/src/components/pages/Conversations/Conversations.vue index 2d231bce..3d0223dd 100644 --- a/src/components/pages/Conversations/Conversations.vue +++ b/src/components/pages/Conversations/Conversations.vue @@ -55,7 +55,7 @@ v-for="item in items" :key="item._id" :item="item" - :call-available="isCallAvailable" + :call-available="isCallEnabled" :blocked-incoming="blockedIncoming(item)" :blocked-outgoing="blockedOutgoing(item)" @start-call="startCall" @@ -188,7 +188,7 @@ ]), ...mapGetters('call', [ 'callState', - 'isCallAvailable' + 'isCallEnabled' ]), noResultsMessage() { if(this.selectedTab === 'call-fax-voicemail') { diff --git a/src/components/pages/Home.vue b/src/components/pages/Home.vue index 4248ee2d..c97b4d70 100644 --- a/src/components/pages/Home.vue +++ b/src/components/pages/Home.vue @@ -53,11 +53,11 @@ :dark="true" :value="callNumberInput" :readonly="dialpadOpened" - :enabled="isCallInitialized" + :enabled="isCallEnabled" @number-changed="numberInputChanged" /> { - Promise.resolve().then(($loadedLibrary)=>{ - this.loadedLibrary = $loadedLibrary; - return this.loadLibrary(); - }).then(()=>{ - return this.createSession(); - }).then(($sessionToken)=>{ - this.sessionToken = $sessionToken; - return this.connectNetwork($sessionToken); - }).then(($network)=>{ + setRtcEngine(rtcEngine) { + if(this.rtcEngine === null) { + this.rtcEngine = rtcEngine; + this.rtcEngine.onSipNetworkConnected(($network)=>{ + this.events.emit('connected'); this.network = $network; this.network.onIncomingCall((remoteCall)=>{ if(this.network !== null && this.currentCall === null) { @@ -85,11 +80,10 @@ export class RtcEngineCall { } this.events.emit('incoming'); }); - resolve(); - }).catch((err)=>{ - reject(err); + }).onSipNetworkDisconnected(()=>{ + this.events.emit('disconnected'); }); - }); + } } isAvailable() { @@ -218,6 +212,16 @@ export class RtcEngineCall { 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(()=>{ @@ -293,8 +297,11 @@ export class RtcEngineCall { } return rtcEngineCallInstance; } +} - static install(Vue) { - Vue.call = RtcEngineCall.getInstance(); +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 new file mode 100644 index 00000000..2c5f29cc --- /dev/null +++ b/src/plugins/conference.js @@ -0,0 +1,47 @@ + +import EventEmitter from 'events' + +let conferencePlugin = null; + +export class ConferencePlugin { + + constructor() { + this.events = new EventEmitter(); + this.rtcEngine = null; + } + + setRtcEngine(rtcEngine) { + if(this.rtcEngine === null) { + this.rtcEngine = rtcEngine; + this.rtcEngine.onConferenceNetworkConnected(()=>{ + this.events.emit('connected'); + }).onConferenceNetworkDisconnected(()=>{ + this.events.emit('disconnected'); + }); + } + } + + onConnected(listener) { + this.events.on('connected', listener); + return this; + } + + onDisconnected(listener) { + this.events.on('disconnected', listener); + return this; + } + + static getInstance() { + if(conferencePlugin === null) { + conferencePlugin = new ConferencePlugin(); + } + return conferencePlugin; + } +} + +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 new file mode 100644 index 00000000..b4d3ce00 --- /dev/null +++ b/src/plugins/rtc-engine.js @@ -0,0 +1,167 @@ + +import config from '../config' +import loadScript from 'load-script' +import EventEmitter from 'events' + +const scriptId = 'cdk'; +const scriptUrl = config.baseHttpUrl + '/rtc/files/dist/cdk-prod.js'; +const webSocketUrl = config.baseWsUrl + '/rtc/api'; + +let rtcEnginePlugin = null; + +export class RtcEnginePlugin { + + constructor() { + this.script = null; + this.client = null; + this.sessionToken = null; + this.ngcpApiJwt = null; + this.events = new EventEmitter(); + } + + createMedia() { + 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; + } + + loadLibrary() { + return new Promise((resolve, reject)=>{ + if(this.script === null) { + loadScript(scriptUrl, { + attrs: { + id: scriptId + } + }, (err, script) => { + this.script = script; + if(err) { + console.error(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) { + cdk.ngcp.setApiJwt(this.ngcpApiJwt); + 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) { + this.client = new cdk.Client({ + url: webSocketUrl, + userSession: this.sessionToken + }); + this.client.onConnect(()=>{ + this.events.emit('connected'); + let conferenceNetwork = this.client.getNetworkByTag('conference'); + conferenceNetwork.onConnect(()=>{ + this.events.emit('conference-network-connected', conferenceNetwork); + }).onDisconnect(()=>{ + this.events.emit('conference-network-disconnected', conferenceNetwork); + }); + let sipNetwork = this.client.getNetworkByTag('sip'); + sipNetwork.onConnect(()=>{ + this.events.emit('sip-network-connected', sipNetwork); + }).onDisconnect(()=>{ + this.events.emit('sip-network-disconnected', sipNetwork); + }); + 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; + } + + static getInstance() { + if(rtcEnginePlugin === null) { + rtcEnginePlugin = new RtcEnginePlugin(); + } + return rtcEnginePlugin; + } +} + +export default { + install(Vue) { + Vue.$rtcEngine = RtcEnginePlugin.getInstance(); + Vue.$rtcEngine.setNgcpApiJwt(localStorage.getItem('jwt')); + } +} diff --git a/src/routes.js b/src/routes.js index 64f7f392..77936452 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,5 +1,8 @@ -import { i18n } from './i18n' +import { + i18n +} from './i18n' +import ConferenceLayout from './components/layouts/Conference' import DefaultLayout from './components/layouts/Default' import Home from './components/pages/Home' import Conversations from './components/pages/Conversations/Conversations' @@ -161,9 +164,25 @@ export default [ title: i18n.t('pages.login.title') } }, + { + path: '/conference', + component: ConferenceLayout, + meta: { + title: 'Conference' + } + }, + { + path: '/conference/:id', + component: ConferenceLayout, + meta: { + title: 'Conference' + } + }, { path: '/', - redirect: {path:'/user/home'} + redirect: { + path:'/user/home' + } }, { path: '*', diff --git a/src/store/call.js b/src/store/call.js index 79a5595d..f16d4bbe 100644 --- a/src/store/call.js +++ b/src/store/call.js @@ -11,9 +11,6 @@ import { import { startCase } from '../filters/string' -import { - RequestState -} from './common' export var CallState = { input: 'input', @@ -54,8 +51,7 @@ function handleUserMediaError(context, err) { export default { namespaced: true, state: { - initializationState: RequestState.initiated, - initializationError: null, + callEnabled: false, endedReason: null, callState: CallState.input, number: '', @@ -72,6 +68,9 @@ export default { dialpadOpened: false }, getters: { + isCallEnabled(state) { + return state.callEnabled; + }, endedReason(state) { return state.endedReason; }, @@ -81,24 +80,27 @@ export default { callNumberInput(state) { return state.numberInput; }, - isNetworkConnected(state) { - return state.initializationState === RequestState.succeeded; - }, - isCallAvailable(state, getters) { - return getters.isNetworkConnected; - }, + // 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 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; + return rootGetters['user/isRtcEngineInitializing']; }, isPreparing(state) { return state.callState === CallState.input; @@ -214,16 +216,6 @@ export default { } }, mutations: { - initRequesting(state) { - state.initializationState = RequestState.requesting; - }, - initSucceeded(state) { - state.initializationState = RequestState.succeeded; - }, - initFailed(state, error) { - state.initializationState = RequestState.failed; - state.initializationError = error; - }, numberInputChanged(state, numberInput) { state.numberInput = numberInput; }, @@ -319,47 +311,30 @@ export default { }, toggleDialpad(state) { state.dialpadOpened = !state.dialpadOpened; + }, + enableCall(state) { + state.callEnabled = true; + }, + disableCall(state) { + state.callEnabled = false; } }, actions: { - initialize(context) { - if(!context.getters.isCallInitialized) { - context.commit('initRequesting'); - Vue.call.onIncoming(()=>{ - context.commit('incomingCall', { - number: Vue.call.getNumber() - }); - }).onRemoteMedia((remoteMediaStream)=>{ - context.commit('establishCall', remoteMediaStream); - }).onEnded((reason)=>{ - Vue.call.end(); - context.commit('endCall', reason); - setTimeout(()=>{ - context.commit('inputNumber'); - }, errorVisibilityTimeout); - }); - Vue.call.initialize().then(()=>{ - context.commit('initSucceeded'); - }).catch((err)=>{ - context.commit('initFailed', err); - }); - } - }, start(context, localMedia) { let number = context.getters.callNumberInput; context.commit('desktopSharingInstallReset'); context.commit('startCalling', number); Promise.resolve().then(()=>{ - return Vue.call.createLocalMedia(localMedia); + return Vue.$call.createLocalMedia(localMedia); }).then((localMediaStream)=>{ context.commit('localMediaSuccess', localMediaStream); - Vue.call.onRingingStart(()=>{ + Vue.$call.onRingingStart(()=>{ context.commit('startRinging'); }).onRingingStop(()=>{ context.commit('stopRinging'); }).start(number, localMediaStream); }).catch((err)=>{ - Vue.call.end(); + Vue.$call.end(); handleUserMediaError(context, err); setTimeout(()=>{ context.commit('inputNumber'); @@ -368,11 +343,11 @@ export default { }, accept(context, localMedia) { context.commit('desktopSharingInstallReset'); - Vue.call.createLocalMedia(localMedia).then((localMediaStream)=>{ - Vue.call.accept(localMediaStream); + Vue.$call.createLocalMedia(localMedia).then((localMediaStream)=>{ + Vue.$call.accept(localMediaStream); context.commit('localMediaSuccess', localMediaStream); }).catch((err)=>{ - Vue.call.end(); + Vue.$call.end(); handleUserMediaError(context, err); setTimeout(()=>{ context.commit('inputNumber'); @@ -380,29 +355,29 @@ export default { }); }, end(context) { - Vue.call.end(); + Vue.$call.end(); context.commit('hangUpCall'); }, sendDTMF(context, value) { - if(Vue.call.hasRunningCall()) { - Vue.call.sendDTMF(value); + if(Vue.$call.hasRunningCall()) { + Vue.$call.sendDTMF(value); } }, toggleMicrophone(context) { if(context.getters.isMicrophoneEnabled) { - Vue.call.disableAudio(); + Vue.$call.disableAudio(); } else { - Vue.call.enableAudio(); + Vue.$call.enableAudio(); } context.commit('toggleMicrophone'); }, toggleCamera(context) { if(context.getters.isCameraEnabled) { - Vue.call.disableVideo(); + Vue.$call.disableVideo(); } else { - Vue.call.enableVideo(); + Vue.$call.enableVideo(); } context.commit('toggleCamera'); }, diff --git a/src/store/conference.js b/src/store/conference.js new file mode 100644 index 00000000..4fb3ff93 --- /dev/null +++ b/src/store/conference.js @@ -0,0 +1,258 @@ + +import Vue from 'vue' +import { + RequestState +} from "./common"; + +const MediaTypes = { + mic: 'mic', + micCam: 'micCam', + cam: 'cam', + micScreen: 'micScreen', + screen: 'screen' +}; + +export default { + namespaced: true, + state: { + conferencingEnabled: false, + microphoneEnabled: false, + cameraEnabled: false, + screenEnabled: false, + localMediaState: RequestState.initiated, + localMediaError: null, + localMediaStream: null, + remoteMediaStreams: [] + }, + getters: { + isJoined() { + return false; + }, + isConferencingEnabled(state) { + return state.conferencingEnabled; + }, + isMicrophoneEnabled(state) { + return state.microphoneEnabled; + }, + isCameraEnabled(state) { + return state.cameraEnabled; + }, + isScreenEnabled(state) { + return state.screenEnabled; + }, + isMediaEnabled(state) { + return state.localMediaStream !== null; + }, + localMediaStream(state) { + if(state.localMediaStream !== null) { + return state.localMediaStream.getStream(); + } + return null; + } + }, + mutations: { + enableConferencing(state) { + state.conferencingEnabled = true; + }, + disableConferencing(state) { + state.conferencingEnabled = false; + }, + enableMicrophone(state) { + state.microphoneEnabled = true; + }, + disableMicrophone(state) { + state.microphoneEnabled = false; + }, + enableCamera(state) { + state.cameraEnabled = true; + }, + disableCamera(state) { + state.cameraEnabled = false; + }, + enableScreen(state) { + state.screenEnabled = true; + }, + disableScreen(state) { + state.screenEnabled = false; + }, + localMediaRequesting(state) { + state.localMediaState = RequestState.requesting; + state.localMediaError = null; + }, + localMediaSucceeded(state, localMediaStream) { + if(state.localMediaStream !== null) { + state.localMediaStream.stop(); + state.localMediaStream = null; + } + state.localMediaState = RequestState.succeeded; + state.localMediaStream = localMediaStream; + state.localMediaError = null; + }, + localMediaFailed(state, error) { + state.localMediaState = RequestState.failed; + state.localMediaError = error; + }, + isLocalMediaRequesting(state) { + return state.localMediaState === RequestState.requesting; + }, + disposeLocalMedia(state) { + if(state.localMediaStream !== null) { + state.localMediaStream.stop(); + state.localMediaStream = null; + state.cameraEnabled = false; + state.microphoneEnabled = false; + state.screenEnabled = false; + } + } + }, + actions: { + createLocalMedia(context, type) { + let media = Vue.$rtcEngine.createMedia(); + context.commit('localMediaRequesting'); + switch(type) { + default: + case MediaTypes.mic: + media.enableMicrophone(); + break; + case MediaTypes.micCam: + media.enableMicrophone(); + media.enableCamera(); + break; + case MediaTypes.micScreen: + media.enableMicrophone(); + media.enableScreen(); + break; + case MediaTypes.cam: + media.enableCamera(); + break; + case MediaTypes.screen: + media.enableScreen(); + break; + } + media.build().then((localMediaStream)=>{ + context.commit('localMediaSucceeded', localMediaStream); + switch(type) { + default: + case MediaTypes.mic: + context.commit('enableMicrophone'); + context.commit('disableCamera'); + context.commit('disableScreen'); + break; + case MediaTypes.micCam: + context.commit('enableMicrophone'); + context.commit('enableCamera'); + context.commit('disableScreen'); + break; + case MediaTypes.micScreen: + context.commit('enableMicrophone'); + context.commit('disableCamera'); + context.commit('enableScreen'); + break; + case MediaTypes.cam: + context.commit('disableMicrophone'); + context.commit('enableCamera'); + context.commit('disableScreen'); + break; + case MediaTypes.screen: + context.commit('disableMicrophone'); + context.commit('disableCamera'); + context.commit('enableScreen'); + break; + } + }).catch((err)=>{ + context.commit('localMediaFailed', err.message); + }); + }, + enableMicrophone(context) { + if(!context.getters.isLocalMediaRequesting) { + let mediaType = MediaTypes.mic; + if(context.getters.isCameraEnabled) { + mediaType = MediaTypes.micCam; + } + else if(context.getters.isScreenEnabled) { + mediaType = MediaTypes.micScreen; + } + context.dispatch('createLocalMedia', mediaType); + } + }, + disableMicrophone(context) { + if(!context.getters.isLocalMediaRequesting) { + let mediaType = null; + if(context.getters.isCameraEnabled) { + mediaType = MediaTypes.cam; + } + else if(context.getters.isScreenEnabled) { + mediaType = MediaTypes.screen; + } + if(mediaType === null) { + context.commit('disposeLocalMedia'); + } + else { + context.dispatch('createLocalMedia', mediaType); + } + } + }, + toggleMicrophone(context) { + if(!context.getters.isMicrophoneEnabled) { + context.dispatch('enableMicrophone'); + } + else { + context.dispatch('disableMicrophone'); + } + }, + enableCamera(context) { + if(!context.getters.isLocalMediaRequesting) { + context.dispatch('createLocalMedia', MediaTypes.micCam); + } + }, + disableCamera(context) { + if(!context.getters.isLocalMediaRequesting) { + let mediaType = null; + if(context.getters.isMicrophoneEnabled) { + mediaType = MediaTypes.mic; + } + if(mediaType === null) { + context.commit('disposeLocalMedia'); + } + else { + context.dispatch('createLocalMedia', mediaType); + } + } + }, + toggleCamera(context) { + if(!context.getters.isCameraEnabled) { + context.dispatch('enableCamera'); + } + else { + context.dispatch('disableCamera'); + } + }, + enableScreen(context) { + if(!context.getters.isLocalMediaRequesting) { + context.dispatch('createLocalMedia', MediaTypes.micScreen); + } + }, + disableScreen(context) { + if(!context.getters.isLocalMediaRequesting) { + let mediaType = null; + if(context.getters.isMicrophoneEnabled) { + mediaType = MediaTypes.mic; + } + if(mediaType === null) { + context.commit('disposeLocalMedia'); + } + else { + context.dispatch('createLocalMedia', mediaType); + } + } + }, + toggleScreen(context) { + if(!context.getters.isScreenEnabled) { + context.dispatch('enableScreen'); + } + else { + context.dispatch('disableScreen'); + } + } + } +} diff --git a/src/store/index.js b/src/store/index.js index bd16a6ff..99441b0e 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,7 +5,7 @@ import Vue from 'vue' import Vuex from 'vuex' import CallBlockingModule from './call-blocking' import CallForwardModule from './call-forward' -import CallModule from './call' +import CallModule, {errorVisibilityTimeout} from './call' import ConversationsModule from './conversations' import PbxConfigModule from './pbx-config/index' import ReminderModule from './reminder' @@ -13,11 +13,17 @@ import SpeedDialModule from './speed-dial' import UserModule from './user' import CommunicationModule from './communication' import VoiceboxModule from './voicebox' - +import ConferenceModule from './conference' import { i18n } from '../i18n'; +import RtcEnginePlugin from "../plugins/rtc-engine"; +import CallPlugin from "../plugins/call"; +import ConferencePlugin from "../plugins/conference"; +Vue.use(RtcEnginePlugin); +Vue.use(CallPlugin); +Vue.use(ConferencePlugin); Vue.use(Vuex); export const store = new Vuex.Store({ @@ -31,9 +37,13 @@ export const store = new Vuex.Store({ speedDial: SpeedDialModule, user: UserModule, communication: CommunicationModule, - voicebox: VoiceboxModule + voicebox: VoiceboxModule, + conference: ConferenceModule }, getters: { + conferenceId(state) { + return _.get(state, 'route.params.id', null); + }, pageTitle(state) { return _.get(state, 'route.meta.title', 'Not defined'); }, @@ -55,5 +65,31 @@ export const store = new Vuex.Store({ title() { return i18n.t('title'); } - } + }, + plugins: [ + 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'); + }); + 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); + }); + } + ] }); diff --git a/src/store/user.js b/src/store/user.js index dd44a24e..5eba774a 100644 --- a/src/store/user.js +++ b/src/store/user.js @@ -1,9 +1,16 @@ 'use strict'; -import { i18n } from '../i18n'; +import Vue from 'vue'; +import { + i18n +} from '../i18n'; import _ from 'lodash'; -import { SessionStorage } from 'quasar-framework' -import { RequestState } from './common' +import { + SessionStorage +} from 'quasar-framework' +import { + RequestState +} from './common' import { login, getUserData @@ -26,6 +33,8 @@ export default { userDataRequesting: false, userDataSucceeded: false, userDataError: null, + rtcEngineInitState: RequestState.initiated, + rtcEngineInitError: null, sessionLocale: null, changeSessionLocaleState: RequestState.initiated, changeSessionLocaleError: null @@ -112,6 +121,12 @@ export default { return null; } }, + isRtcEngineInitialized(state) { + return state.rtcEngineInitState === RequestState.succeeded; + }, + isRtcEngineInitializing(state) { + return state.rtcEngineInitState === RequestState.requesting; + }, changeSessionLocaleState(state) { return state.changeSessionLocaleState; } @@ -163,6 +178,16 @@ export default { state.userDataSucceeded = false; state.userDataError = null; }, + rtcEngineInitRequesting(state) { + state.rtcEngineInitState = RequestState.requesting; + }, + rtcEngineInitSucceeded(state) { + state.rtcEngineInitState = RequestState.succeeded; + }, + rtcEngineInitFailed(state, error) { + state.rtcEngineInitState = RequestState.failed; + state.rtcEngineInitError = error; + }, changeSessionLocaleRequesting(state) { state.changeSessionLocaleState = RequestState.requesting; state.changeSessionLocaleError = null; @@ -217,8 +242,12 @@ export default { }, context.getters.jwtTTL * 1000); } if(context.getters.hasRtcEngineCapabilityEnabled) { - context.dispatch('call/initialize', null, { - root: true + context.commit('rtcEngineInitRequesting'); + Vue.$rtcEngine.setNgcpApiJwt(localStorage.getItem('jwt')); + Vue.$rtcEngine.initialize().then(()=>{ + context.commit('rtcEngineInitSucceeded'); + }).catch((err)=>{ + context.commit('rtcEngineInitFailed', err.message); }); } }).catch((err)=>{ diff --git a/src/themes/app.common.styl b/src/themes/app.common.styl index 788d1e9a..ccebdf8b 100644 --- a/src/themes/app.common.styl +++ b/src/themes/app.common.styl @@ -190,6 +190,19 @@ .csc-item-highlight:hover background alpha($primary, 0.3) !important +.csc-conf-full-height + position relative + top 0 + min-height "calc(100vh - %s)" % (2 * $call-footer-height + $call-footer-height / 2) + +.csc-button.q-btn +.csc-conf-button.q-btn + .q-btn-inner + color black + +input.q-input-target + height auto + .csc-additional padding-right $flex-gutter-lg padding-bottom $flex-gutter-xs