TT#23572 Call: As a Customer, I want to be able to receive an audio/video call

Change-Id: Ifb0dc96cb06ef1720d36c42fb346f5f01e1d617a
changes/13/17313/6
Hans-Peter Herzog 8 years ago
parent e1689379b4
commit 3db9b924d7

@ -12,14 +12,18 @@
<span v-else-if="isInitiating" class="text">{{ $t('call.initiating') }}</span>
<span v-else-if="isRinging" class="text">{{ $t('call.ringing') }}</span>
<span v-else-if="isEnded" class="text">{{ $t('call.ended') }}</span>
<span v-else-if="isIncoming" class="text">Incoming call</span>
<span v-else class="text">{{ $t('call.call') }}</span>
<q-btn round small slot="right" class="no-shadow" @click="close()" icon="clear"/>
</q-card-title>
<q-card-main>
<div v-if="isTrying" class="csc-spinner"><q-spinner-rings color="primary" :size="60" /></div>
<div v-if="isCalling" class="phone-number">{{ getNumber }}</div>
<csc-media v-if="isCalling && localMediaStream != null" :stream="localMediaStream" />
<div v-if="isRinging" class="csc-spinner"><q-spinner-rings color="primary" :size="60" /></div>
<div v-if="!isPreparing" class="phone-number">{{ getNumber | numberFormat }}</div>
<csc-media id="local-media" v-show="isCalling" :stream="localMediaStream" />
<csc-media id="remote-media" v-show="isEstablished" :stream="remoteMediaStream" />
<q-field v-if="isPreparing" :helper="$t('call.inputNumber')" :error="validationEnabled && phoneNumberError"
:error-label="$t('call.inputValidNumber')" :count="64" dark>
<q-input :float-label="$t('call.number')" v-model="formattedPhoneNumber" dark clearable max="64"
@ -34,6 +38,9 @@
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioVideo')" icon="videocam" />
<q-btn v-if="isCalling" round small color="negative" @click="hangUp()" icon="call end" />
<q-btn v-if="isEnded" round small color="negative" @click="init()" icon="clear"/>
<q-btn v-if="isIncoming" round small color="primary" @click="accept('audioOnly')" icon="mic" />
<q-btn v-if="isIncoming" round small color="primary" @click="accept('audioVideo')" icon="videocam" />
<q-btn v-if="isIncoming" round small color="negative" @click="decline()" icon="call end" />
</q-card-actions>
</q-card>
@ -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',

@ -1,34 +1,65 @@
<template>
<div class="csc-media">
<video ref="media"></video>
<div v-show="loading" class="csc-spinner">
<q-spinner-mat color="primary" :size="60" />
</div>
<video v-show="!loading && hasVideo" ref="media" autoplay></video>
</div>
</template>
<script>
import _ from 'lodash';
import { QSpinnerMat } from 'quasar-framework'
export default {
name: 'csc-media',
props: ['stream'],
data () {
return {
currentStream: this.stream
currentStream: this.stream,
loading: true,
}
},
mounted() {},
components: {
QSpinnerMat
},
methods: {
assignStream(stream) {
this.currentStream = stream;
if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.srcObject)) {
this.$refs.media.srcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.mozSrcObject)) {
this.$refs.media.mozSrcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
_.isObject(URL) && _.isFunction(URL.createObjectURL)) {
this.$refs.media.src = URL.createObjectURL(this.currentStream);
}
let timer = setInterval(()=>{
if(this.$refs.media.currentTime > 0) {
this.loading = false;
clearInterval(timer);
}
}, 100);
}
},
mounted() {
if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.srcObject)) {
this.$refs.media.srcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
!_.isUndefined(this.$refs.media.mozSrcObject)) {
this.$refs.media.mozSrcObject = this.currentStream;
} else if(_.isObject(this.currentStream) && _.isObject(this.$refs.media) &&
_.isObject(URL) && _.isFunction(URL.createObjectURL)) {
this.$refs.media.src = URL.createObjectURL(this.currentStream);
watch: {
stream() {
if(_.isObject(this.stream) && this.currentStream !== this.stream) {
this.loading = true;
this.assignStream(this.stream);
}
}
},
components: {},
methods: {},
computed: {}
computed: {
hasVideo() {
return _.isArray(this.currentStream.getVideoTracks()) &&
this.currentStream.getVideoTracks().length > 0;
}
}
}
</script>
@ -39,4 +70,7 @@
.csc-media video {
width: 100%
}
.csc-media .csc-spinner {
}
</style>

@ -35,9 +35,6 @@
if(this.right) {
classes.push('page-title-right');
}
console.log(classes);
return classes;
},
...mapGetters('layout', ['left', 'right'])

@ -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';

@ -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);

@ -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 '';
}

@ -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?"
}
}

@ -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;
}
}

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

Loading…
Cancel
Save