-
- {{ 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');
}
}
};