diff --git a/src/components/CscCall.vue b/src/components/CscCall.vue index 8a1ceda6..f13a74d2 100644 --- a/src/components/CscCall.vue +++ b/src/components/CscCall.vue @@ -12,14 +12,18 @@ {{ $t('call.initiating') }} {{ $t('call.ringing') }} {{ $t('call.ended') }} + Incoming call {{ $t('call.call') }} -
-
{{ getNumber }}
- +
+
{{ getNumber | numberFormat }}
+ + + + + + + @@ -45,16 +52,14 @@ import { mapState, mapGetters } from 'vuex' import CscMedia from './CscMedia' import { QLayout, QCard, QCardTitle, QCardSeparator, QCardMain, QField, QInput, - QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings } from 'quasar-framework' - import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' - var phoneUtil = PhoneNumberUtil.getInstance(); + QCardActions, QBtn, QIcon, Loading, Alert, QSpinnerRings, Dialog } from 'quasar-framework' + import { normalizeNumber, rawNumber } from '../filters/number-format' export default { name: 'csc-call', props: ['region'], data () { return { phoneNumber: '', - parsedPhoneNumber: null, phoneNumberError: false, validationEnabled: false } @@ -79,12 +84,12 @@ QBtn, QIcon, QSpinnerRings, - CscMedia + CscMedia, + Dialog }, methods: { init() { this.phoneNumber = ''; - this.parsedPhoneNumber = null; this.validationEnabled = false; this.phoneNumberError = false; this.$store.commit('call/inputNumber'); @@ -97,7 +102,7 @@ }, call(localMedia) { this.validationEnabled = true; - if(this.parsedPhoneNumber !== null) { + if(this.phoneNumber !== null) { this.phoneNumberError = false; this.$store.dispatch('call/start', { number: this.phoneNumber, @@ -107,12 +112,37 @@ this.phoneNumberError = true; } }, + accept(localMedia) { + this.$store.dispatch('call/accept', localMedia); + }, + decline() { + this.hangUp(); + this.$emit('close'); + }, hangUp() { this.$store.dispatch('call/hangUp'); }, close() { - this.$store.commit('call/inputNumber'); - this.$emit('close'); + if(this.isPreparing || this.isEnded) { + this.init(); + this.$emit('close'); + } else { + Dialog.create({ + title: this.$t('call.endCall'), + message: this.$t('call.endCallDialog'), + buttons: [ + 'Cancel', + { + label: this.$t('call.endCall'), + color: 'negative', + handler: ()=>{ + this.hangUp(); + this.$emit('close'); + } + } + ] + }); + } }, playIncomingSound() { this.$refs.incomingRinging.play(); @@ -124,44 +154,26 @@ computed: { formattedPhoneNumber: { get() { - if(this.parsedPhoneNumber !== null) { - return _.trim(phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.INTERNATIONAL)); - } else { - return _.trim(this.phoneNumber); - } + return normalizeNumber(this.phoneNumber); }, set(value) { this.validationEnabled = true; - this.phoneNumber = _.trim(value); - if(this.phoneNumber.match('^[1-9]')) { - this.phoneNumber = '+' + this.phoneNumber; - } else if(this.phoneNumber === '+') { - this.phoneNumber = ''; - } - if(phoneUtil.isPossibleNumberString(this.phoneNumber, this.region)) { - try { - this.parsedPhoneNumber = phoneUtil.parse(this.phoneNumber, this.region); - this.phoneNumber = phoneUtil.format(this.parsedPhoneNumber, PhoneNumberFormat.E164); - this.phoneNumberError = false; - } catch(err) { - this.parsedPhoneNumber = null; - this.phoneNumberError = true; - } - } else { - this.parsedPhoneNumber = null; - this.phoneNumberError = true; - } + this.phoneNumber = rawNumber(value); } }, localMediaStream() { - if(this.$store.state.call.localMediaStream != null) { + if(this.$store.state.call.localMediaStream !== null) { return this.$store.state.call.localMediaStream.getStream(); } else { return null; } }, remoteMediaStream() { - console.log(this.$refs.remoteMedia); + if(this.$store.state.call.remoteMediaStream !== null) { + return this.$store.state.call.remoteMediaStream.getStream(); + } else { + return null; + } }, ...mapGetters('call', [ 'isPreparing', @@ -170,6 +182,8 @@ 'isRinging', 'isCalling', 'isEnded', + 'isIncoming', + 'isEstablished', 'getNumber', 'getMediaType', 'getLocalMediaType', diff --git a/src/components/CscMedia.vue b/src/components/CscMedia.vue index 20a2edc1..83a8b4f4 100644 --- a/src/components/CscMedia.vue +++ b/src/components/CscMedia.vue @@ -1,34 +1,65 @@ @@ -39,4 +70,7 @@ .csc-media video { width: 100% } + .csc-media .csc-spinner { + + } diff --git a/src/components/CscPage.vue b/src/components/CscPage.vue index 3849a96d..8f517b06 100644 --- a/src/components/CscPage.vue +++ b/src/components/CscPage.vue @@ -35,9 +35,6 @@ if(this.right) { classes.push('page-title-right'); } - - console.log(classes); - return classes; }, ...mapGetters('layout', ['left', 'right']) diff --git a/src/components/pages/Conversations.vue b/src/components/pages/Conversations.vue index a650a4b0..0efd3f49 100644 --- a/src/components/pages/Conversations.vue +++ b/src/components/pages/Conversations.vue @@ -53,6 +53,7 @@ import CscCollapsible from '../card/CscCollapsible' import { QBtn, QCardActions, QCard, QCardSeparator, QInfiniteScroll, QPopover, QList, QItem, QSpinnerDots } from 'quasar-framework' + import numberFormat from '../../filters/number-format' export default { data () { return { @@ -78,8 +79,7 @@ }, methods: { call(conversation, localMedia) { - let number = conversation.direction == 'out' ? - conversation.callee : conversation.caller; + let number = conversation.direction == 'out' ? conversation.callee : conversation.caller; this.$store.dispatch('call/start', { number: number, localMedia: localMedia }); }, @@ -120,7 +120,11 @@ let direction = item.direction == 'in' ? this.$t('pages.conversations.labels.from') : this.$t('pages.conversations.labels.to'); - return `${prefix} ${item.type} ${direction} ${item.caller}`; + let number = item.caller; + if(item.direction === 'out') { + number = item.callee; + } + return `${prefix} ${item.type} ${direction} ${numberFormat(number)}`; }, isCall(type) { return type == 'call'; diff --git a/src/filters/index.js b/src/filters/index.js index ae1159be..48031014 100644 --- a/src/filters/index.js +++ b/src/filters/index.js @@ -1,7 +1,9 @@ import Vue from 'vue'; import NumberFilter from './number' +import NumberFormatFilter from './number-format' import DateFilter from './date' Vue.filter('number', NumberFilter); Vue.filter('readableDate', DateFilter); +Vue.filter('numberFormat', NumberFormatFilter); diff --git a/src/filters/number-format.js b/src/filters/number-format.js new file mode 100644 index 00000000..3792bc06 --- /dev/null +++ b/src/filters/number-format.js @@ -0,0 +1,37 @@ + + +import url from 'url'; +import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'; +var phoneUtil = PhoneNumberUtil.getInstance(); + +export default function(number) { + try { + let phoneNumber = url.parse(number, true).auth.split(':')[0]; + return normalizeNumber(phoneNumber); + } catch(err1) { + return normalizeNumber(number); + } +} + +export function normalizeNumber(number) { + if(_.isString(number) && number.match(/^\+?[0-9]+$/)) { + let normalizedNumber = number.replace(/\s*/g, ''); + if(normalizedNumber.match(/^\+/) === null) { + normalizedNumber = '+' + normalizedNumber; + } + try { + return phoneUtil.format(phoneUtil.parse(normalizedNumber, 'DE'), PhoneNumberFormat.INTERNATIONAL); + } catch(err) { + return normalizedNumber; + } + } else { + return number; + } +} + +export function rawNumber(number) { + if(_.isString(number)) { + return number.replace(/\s*/g, '').replace(/^\+/, ''); + } + return ''; +} diff --git a/src/locales/en.json b/src/locales/en.json index 2ccb343d..7415a983 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -123,6 +123,8 @@ "call": "Call", "inputNumber": "Input a phone number", "inputValidNumber": "Input a valid phone number", - "number": "Number" + "number": "Number", + "endCall": "End Call", + "endCallDialog": "You are about to end the current call. Are you sure?" } } diff --git a/src/plugins/call.js b/src/plugins/call.js index e285fdad..7de40fe4 100644 --- a/src/plugins/call.js +++ b/src/plugins/call.js @@ -1,6 +1,7 @@ +import EventEmitter from 'events'; import _ from 'lodash'; -import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib'; +import { loadCdkLib, connectCdkNetwork } from '../helpers/cdk-lib'; import { createSessionToken } from '../api/rtcsession'; export const LocalMedia = { @@ -11,26 +12,45 @@ export const LocalMedia = { screenOnly: 'screenOnly' }; -export class NetworkNotConnected extends Error { +export class NetworkNotConnected { constructor(network) { - super(); - this.name = this.constructor.name; + this.name = "NetworkNotConnected"; this.message = 'Network ' + network + ' is not connected'; this.network = network; } } +export class CallNotFound { + + constructor() { + this.name = "CallNotFound"; + this.message = 'Call not found'; + } +} + +export class CallAlreadyExists { + + constructor() { + this.name = "CallAlreadyExists"; + this.message = 'Call already exists'; + } +} + var rtcEngineCallInstance = null; export class RtcEngineCall { constructor(options) { this.networkTag = 'sip'; - this.client = null; this.network = null; - this.currentCall = null; this.loadedLibrary = null; this.sessionToken = null; + this.localCall = null; + this.localMedia = null; + this.remoteCall = null; + this.remoteMedia = null; + this.events = new EventEmitter(); + this.endedReason = null; } initialize() { @@ -45,6 +65,19 @@ export class RtcEngineCall { return this.connectNetwork($sessionToken); }).then(($network)=>{ this.network = $network; + this.network.onIncomingCall((remoteCall)=>{ + if(this.network !== null && this.remoteCall === null) { + this.remoteCall = remoteCall; + this.remoteCall.onEnded(()=>{ + this.events.emit('ended', this.remoteCall.endedReason); + }).onRemoteMedia((remoteMediaStream)=>{ + this.events.emit('remoteMedia', remoteMediaStream); + }).onRemoteMediaEnded(()=>{ + this.events.emit('remoteMediaEnded'); + }); + } + this.events.emit('incoming'); + }); resolve(); }).catch((err)=>{ reject(err); @@ -57,7 +90,7 @@ export class RtcEngineCall { } hasRunningCall() { - return this.currentCall !== null; + return this.localCall !== null || this.remoteCall !== null; } loadLibrary() { @@ -69,12 +102,12 @@ export class RtcEngineCall { } connectNetwork(session) { - return connectDefaultCdkNetwork(session); + return connectCdkNetwork(session, this.networkTag); } createLocalMedia(localMedia) { return new Promise((resolve, reject)=>{ - var localMediaStream = new cdk.LocalMediaStream(); + this.localMedia = new cdk.LocalMediaStream(); var hasAudio = localMedia === LocalMedia.audioOnly || localMedia === LocalMedia.audioVideo || localMedia === LocalMedia.audioScreen; @@ -83,61 +116,137 @@ export class RtcEngineCall { var hasScreen = localMedia === LocalMedia.audioScreen || localMedia === LocalMedia.screenOnly; - localMediaStream.queryMediaSources((sources) => { + this.localMedia.queryMediaSources((sources) => { if (hasAudio && _.isObject(sources.defaultAudio)) { - localMediaStream.setAudio(sources.defaultAudio); + this.localMedia.setAudio(sources.defaultAudio); } if (hasVideo && _.isObject(sources.defaultVideo)) { - localMediaStream.setVideo(sources.defaultVideo); + this.localMedia.setVideo(sources.defaultVideo); } else if (hasScreen && _.isObject(sources.desktopSharing)) { - localMediaStream.setVideo(sources.desktopSharing); + this.localMedia.setVideo(sources.desktopSharing); } }); - - localMediaStream.build((err)=>{ + this.localMedia.build((err)=>{ if(_.isObject(err)) { reject(err); } else { - resolve(localMediaStream); + resolve(this.localMedia); } }); }); } start(peer, localMediaStream) { - peer = peer.replace('+', ''); - if(this.network !== null) { - this.currentCall = this.network.call(peer, { localMediaStream: localMediaStream }); - return this.currentCall; + if(this.network !== null && this.localCall === null) { + peer = peer.replace(/(\s|\+)/,''); + this.localCall = this.network.call(peer, { + localMediaStream: localMediaStream + }); + this.localCall.onEnded(()=>{ + this.events.emit('ended', this.localCall.endedReason); + this.end(); + }).onPending(()=>{ + this.events.emit('pending'); + }).onRemoteMedia((remoteMediaStream)=>{ + this.events.emit('remoteMedia', remoteMediaStream); + }).onRemoteMediaEnded(()=>{ + this.events.emit('remoteMediaEnded'); + }).onRingingStart(()=>{ + this.events.emit('ringingStart'); + }).onRingingStop(()=>{ + this.events.emit('ringingStop'); + }); + } else if(this.network !== null) { + throw new CallAlreadyExists(); } else { throw new NetworkNotConnected(this.networkTag); } } - onIncoming(listener) { - if(this.network !== null) { - this.network.onIncomingCall((call)=>{ - if(this.currentCall === null) { - this.currentCall = call; - listener(call); - } - }); + getNumber() { + if(this.localCall !== null) { + return this.localCall.peer; + } else if(this.remoteCall !== null) { + return this.remoteCall.peer; } else { - throw new NetworkNotConnected(this.networkTag); + return null; } } + getEndedReason() { + return this.endedReason; + } + + fetchEndedReason() { + if(this.localCall !== null) { + return this.localCall.endedReason; + } else if(this.remoteCall !== null) { + return this.remoteCall.endedReason; + } else { + return null; + } + } + + onIncoming(listener) { + this.events.on('incoming', 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; + } + accept(localMediaStream) { - if(this.currentCall !== null) { - this.currentCall.accept({ + if(this.remoteCall !== null) { + this.remoteCall.accept({ localMediaStream: localMediaStream }); } } hangUp() { - if(this.currentCall !== null) { - this.currentCall.end(); + this.end(); + } + + end() { + this.endedReason = this.fetchEndedReason(); + if(this.localCall !== null) { + this.localCall.end(); + this.localCall = null; + } + if(this.remoteCall !== null) { + this.remoteCall.end(); + this.remoteCall = null; + } + if(this.localMedia !== null) { + this.localMedia.stop(); + this.localMedia = null; } } diff --git a/src/store/call.js b/src/store/call.js index 3bccca8f..c96f6fed 100644 --- a/src/store/call.js +++ b/src/store/call.js @@ -58,6 +58,9 @@ export default { isInitiating(state, getters) { return state.callState === CallState.initiating; }, + isIncoming(state, getters) { + return state.callState === CallState.incoming; + }, isTrying(state, getters) { return state.callState === CallState.initiating || state.callState === CallState.ringing; @@ -70,6 +73,9 @@ export default { state.callState === CallState.ringing || state.callState === CallState.established; }, + isEstablished(state, getters) { + return state.callState === CallState.established; + }, isEnded(state, getters) { return state.callState === CallState.ended; } @@ -90,21 +96,21 @@ export default { state.number = options.number; state.mediaType = options.mediaType; state.localMediaType = state.mediaType; - state.localMediaStream = options.localMediaStream; state.callState = CallState.initiating; }, - acceptIncoming(state, options) { - state.localMediaStream = options.localMediaStream; + localMediaSuccess(state, localMediaStream) { + state.localMediaStream = localMediaStream; }, startRinging(state) { state.callState = CallState.ringing; }, - establishCall(state, options) { - state.remoteMediaStream = options.remoteMediaStream; + establishCall(state, remoteMediaStream) { + state.remoteMediaStream = remoteMediaStream; state.callState = CallState.established; }, incomingCall(state, options) { state.callState = CallState.incoming; + state.number = options.number; }, hangUpCall(state) { state.callState = CallState.input; @@ -133,20 +139,21 @@ export default { actions: { initialize(context) { return new Promise((resolve, reject)=>{ + Vue.call.onIncoming(()=>{ + context.commit('layout/showRight', null, { root: true }); + context.commit('incomingCall', { + number: Vue.call.getNumber() + }); + }).onRemoteMedia((remoteMediaStream)=>{ + context.commit('establishCall', remoteMediaStream); + }).onRemoteMediaEnded(()=>{ + context.commit("endRemoteMedia"); + }).onEnded(()=>{ + Vue.call.end(); + context.commit('endCall', Vue.call.getEndedReason()); + }); Vue.call.initialize().then(()=>{ context.commit('initSucceeded'); - Vue.call.onIncoming((call)=>{ - context.commit('incomingCall'); - call.onRemoteMedia((remoteMediaStream)=>{ - context.commit('establishCall', { - remoteMediaStream: remoteMediaStream - }); - }).onRemoteMediaEnded(()=>{ - context.commit("endRemoteMedia"); - }).onEnded(()=>{ - context.commit('endCall', call.endedReason); - }); - }); resolve(); }).catch((err)=>{ context.commit('initFailed', err); @@ -161,54 +168,36 @@ export default { * @param options.number */ start(context, options) { - console.log('start()'); - console.log('options.number is', options.number, 'and options.localMedia is', options.localMedia); context.commit('layout/showRight', null, { root: true }); - Vue.call.createLocalMedia(options.localMedia).then((localMediaStream)=>{ - console.log('Vue.call.createLocalMediai()'); - var call = Vue.call.start(options.number, localMediaStream); - call.onAccepted(()=>{ - }).onEnded(()=>{ - context.commit('endCall', call.endedReason); - - }).onPending(()=>{ - context.commit('startCalling', { - number: options.number, - mediaType: options.localMedia, - localMediaStream: localMediaStream - }); - }).onRemoteMedia((remoteMediaStream)=>{ - context.commit('establishCall', { - remoteMediaStream: remoteMediaStream - }); - }).onRemoteMediaEnded(()=>{ - context.commit("endRemoteMedia"); - }).onRingingStart(()=>{ - context.commit('startRinging'); - }).onRingingStop(()=>{ - context.commit('stopRinging'); - }); + context.commit('startCalling', { + number: options.number, + mediaType: options.localMedia }); + Promise.resolve().then(()=>{ + return Vue.call.createLocalMedia(options.localMedia); + }).then((localMediaStream)=>{ + context.commit('localMediaSuccess', localMediaStream); + Vue.call.onRingingStart(()=>{ + context.commit('startRinging'); + }).onRingingStop(()=>{ + context.commit('stopRinging'); + }).start(options.number, localMediaStream); }).catch((err)=>{ context.commit('endCall', err.name); + Vue.call.end(); }); }, accept(context, localMedia) { Vue.call.createLocalMedia(localMedia).then((localMediaStream)=>{ Vue.call.accept(localMediaStream); - context.commit('acceptIncoming', { - localMediaStream: localMediaStream - }); + context.commit('localMediaSuccess', localMediaStream); }).catch((err)=>{ + Vue.call.end(); context.commit('endCall', 'localMediaError'); }); }, hangUp(context) { - if(Vue.call.hasRunningCall()) { - Vue.call.hangUp(); - context.commit('hangUpCall'); - } else { - context.commit('endCall', 'noRunningCall'); - } + Vue.call.hangUp(); + context.commit('hangUpCall'); } } };