TT#23608 Call: As a Customer, I want be able to see an established call

Change-Id: I9f2fa9e86f4d6ece7e98ec5f4509a56d79be583c
changes/80/18680/2
Hans-Peter Herzog 8 years ago
parent 9567594bd1
commit 02c8221a97

@ -3,44 +3,62 @@
<audio ref="incomingSound" loop preload="auto" src="statics/ring.mp3"></audio>
<q-card flat color="secondary">
<q-card-title>
<q-icon v-if="isCalling && getMediaType == 'audioOnly'" name="mic" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && getMediaType == 'audioVideo'" name="videocam" color="primary" size="26px"/>
<q-icon v-else-if="isEnded" name="error" color="primary" size="26px"/>
<q-icon v-else name="call made" color="primary" size="26px"/>
<span v-if="isRinging || isInitiating || isIncoming">
<q-spinner-rings color="primary" :size="50" />
</span>
<q-icon v-if="isEnded" name="error" color="primary" size="26px"/>
<span v-if="isPreparing" class="text">{{ $t('call.startNew') }}</span>
<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-if="isIncoming" class="text">{{ $t('call.incoming') }}</span>
<span v-else class="text">{{ $t('call.call') }}</span>
<q-btn round small slot="right" class="no-shadow" @click="close()" icon="clear"/>
<q-btn v-if="isFullscreenEnabled" round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen exit"/>
<q-btn v-else round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="toggleFullscreen()" icon="fullscreen"/>
<q-btn round :small="!isFullscreenEnabled" slot="right"
class="no-shadow" @click="close()" icon="clear"/>
</q-card-title>
<q-card-main>
<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"
@blur="phoneNumberBlur()" @focus="phoneNumberFocus()"/>
</q-field>
<div v-if="isEnded" class="ended-reason">
{{ getEndedReason }}
<div class="csc-call-info">
<q-field v-if="isPreparing" :helper="$t('call.inputNumber')" :count="64" dark
:error="validationEnabled && phoneNumberError" :error-label="$t('call.inputValidNumber')">
<q-input :float-label="$t('call.number')" v-model.trim="formattedPhoneNumber"
dark clearable max="64" @blur="phoneNumberBlur()" @focus="phoneNumberFocus()"/>
</q-field>
<div v-if="!isPreparing" class="phone-number">
<q-icon v-if="isCalling && getMediaType == 'audioOnly'" name="mic" color="primary" size="26px"/>
<q-icon v-else-if="isCalling && getMediaType == 'audioVideo'" name="videocam" color="primary" size="26px"/>
{{ getNumber | numberFormat }}
</div>
<div v-if="isEnded" class="ended-reason">{{ getEndedReason }}</div>
</div>
<div class="csc-call-media">
<csc-media :class="mediaPreviewClasses" id="local-media" :muted="true"
v-show="isCalling" :stream="localMediaStream" />
<csc-media class="csc-media-remote" id="remote-media" :muted="isMuted"
v-show="isEstablished" :stream="remoteMediaStream" />
</div>
</q-card-main>
<q-card-actions align="center">
<q-btn v-if="isPreparing" round small color="primary" @click="call('audioOnly')" icon="mic" />
<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-btn v-if="isEstablished" round :small="!isFullscreenEnabled" color="primary" @click="toggleAudio()" :icon="toggleAudioIcon" />
<q-btn v-if="isEstablished" round :small="!isFullscreenEnabled" color="primary" @click="toggleVideo()" :icon="toggleVideoIcon" />
<q-btn v-if="isEstablished" round :small="!isFullscreenEnabled" color="primary" @click="toggleMute()" :icon="toggleMuteIcon" />
<q-btn v-if="isPreparing" round :small="!isFullscreenEnabled" color="primary" @click="call('audioOnly')" icon="mic" />
<q-btn v-if="isPreparing" round :small="!isFullscreenEnabled" color="primary" @click="call('audioVideo')" icon="videocam" />
<q-btn v-if="isPreparing" round :small="!isFullscreenEnabled" color="primary" @click="call('audioScreen')" icon="computer" />
<q-btn v-if="isCalling" round :small="!isFullscreenEnabled" color="negative" @click="hangUp()" icon="call end" />
<q-btn v-if="isEnded" round :small="!isFullscreenEnabled" color="negative" @click="init()" icon="clear"/>
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioOnly')" icon="mic" />
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioVideo')" icon="videocam" />
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="primary" @click="accept('audioScreen')" icon="computer" />
<q-btn v-if="isIncoming" round :small="!isFullscreenEnabled" color="negative" @click="decline()" icon="call end" />
</q-card-actions>
</q-card>
@ -56,7 +74,7 @@
import { normalizeNumber, rawNumber } from '../filters/number-format'
export default {
name: 'csc-call',
props: ['region'],
props: ['region', 'fullscreen'],
data () {
return {
phoneNumber: '',
@ -102,7 +120,7 @@
},
call(localMedia) {
this.validationEnabled = true;
if(this.phoneNumber !== null) {
if(!_.isEmpty(this.phoneNumber)) {
this.phoneNumberError = false;
this.$store.dispatch('call/start', {
number: this.phoneNumber,
@ -149,15 +167,71 @@
},
stopIncomingSound() {
this.$refs.incomingRinging.stop();
},
toggleAudio() {
if(this.isAudioEnabled) {
this.$store.dispatch('call/disableAudio');
} else {
this.$store.dispatch('call/enableAudio');
}
},
toggleVideo() {
if(this.isVideoEnabled) {
this.$store.dispatch('call/disableVideo');
} else {
this.$store.dispatch('call/enableVideo');
}
},
toggleMute() {
if(this.isMuted) {
this.$store.commit('call/unmute');
} else {
this.$store.commit('call/mute');
}
},
toggleFullscreen() {
this.$emit('fullscreen');
}
},
computed: {
isFullscreenEnabled() {
return this.fullscreen;
},
toggleAudioIcon() {
if(this.isAudioEnabled) {
return 'mic'
} else {
return 'mic off';
}
},
toggleVideoIcon() {
if(this.isVideoEnabled) {
return 'videocam'
} else {
return 'videocam off';
}
},
toggleMuteIcon() {
if(this.isMuted) {
return 'volume off'
} else {
return 'volume up';
}
},
mediaPreviewClasses() {
var classes = [];
if(this.isEstablished && this.hasRemoteVideo) {
classes.push('csc-media-preview');
}
return classes;
},
formattedPhoneNumber: {
get() {
return normalizeNumber(this.phoneNumber);
},
set(value) {
this.validationEnabled = true;
this.phoneNumberError = false;
this.phoneNumber = rawNumber(value);
}
},
@ -187,7 +261,12 @@
'getNumber',
'getMediaType',
'getLocalMediaType',
'getEndedReason'
'getEndedReason',
'hasRemoteVideo',
'hasVideo',
'isAudioEnabled',
'isVideoEnabled',
'isMuted'
]),
hasLocalVideo() {
return this.getLocalMediaType !== null && this.getLocalMediaType.match(/(v|V)ideo/) !== null;
@ -197,6 +276,13 @@
</script>
<style lang="stylus">
@import '../../src/themes/app.variables.styl';
@import '../../src/themes/quasar.variables.styl';
.csc-call {
width: inherit;
}
.csc-call .q-card {
margin:0;
}
@ -223,6 +309,9 @@
.q-card-title .text {
color: #adb3b8;
}
.csc-call-fullscreen .csc-call .q-card-title .text {
color: white;
}
.csc-call .phone-number {
font-size: 18px;
@ -231,10 +320,26 @@
margin-bottom: 16px;
}
.csc-call-fullscreen .csc-call .phone-number {
color: white;
}
.csc-call .ended-reason {
font-size: 18px;
text-align: center;
color: #adb3b8;
}
.csc-call-media {
position: relative;
}
.csc-media.csc-media-preview {
position: absolute;
bottom: 0;
left: 0;
width: 25%;
z-index: 10;
}
</style>

@ -3,18 +3,18 @@
<div v-show="loading" class="csc-spinner">
<q-spinner-mat color="primary" :size="60" />
</div>
<video v-show="!loading && hasVideo" ref="media" autoplay></video>
<video v-show="!loading && hasVideo" ref="media" autoplay :muted="muted"></video>
</div>
</template>
<script>
import _ from 'lodash';
import { QSpinnerMat } from 'quasar-framework'
import { QSpinnerMat, QIcon } from 'quasar-framework'
export default {
name: 'csc-media',
props: ['stream'],
props: ['stream', 'muted'],
data () {
return {
currentStream: this.stream,
@ -23,7 +23,8 @@
},
mounted() {},
components: {
QSpinnerMat
QSpinnerMat,
QIcon
},
methods: {
assignStream(stream) {
@ -39,7 +40,7 @@
this.$refs.media.src = URL.createObjectURL(this.currentStream);
}
let timer = setInterval(()=>{
if(this.$refs.media.currentTime > 0) {
if(this.currentStream !== null || this.$refs.media.currentTime > 0) {
this.loading = false;
clearInterval(timer);
}
@ -52,11 +53,14 @@
this.loading = true;
this.assignStream(this.stream);
}
},
muted() {
this.$refs.media.muted = this.muted;
}
},
computed: {
hasVideo() {
return _.isArray(this.currentStream.getVideoTracks()) &&
return this.currentStream !== null && _.isArray(this.currentStream.getVideoTracks()) &&
this.currentStream.getVideoTracks().length > 0;
}
}
@ -65,12 +69,10 @@
<style lang="stylus">
.csc-media {
width: 100%
position: relative;
}
.csc-media video {
width: 100%
}
.csc-media .csc-spinner {
position: relative;
width: 100%;
}
</style>

@ -1,5 +1,6 @@
<template>
<q-layout ref="layout" view="lHh LpR lFf" :right-breakpoint="1100" v-model="layout">
<q-layout ref="layout" :view="layoutView" :right-breakpoint="1100"
@right-breakpoint="rightBreakPoint" :right-class="callClasses">
<q-toolbar slot="header">
<q-btn flat @click="$refs.layout.toggleLeft()">
<q-icon name="menu"/>
@ -96,7 +97,8 @@
</q-fab-action>
</q-fab>
</q-fixed-position>
<csc-call ref="cscCall" slot="right" @close="$refs.layout.hideRight()" region="DE" />
<csc-call ref="cscCall" slot="right" @close="closeCall()" @fullscreen="toggleFullscreen()"
:fullscreen="isFullscreenEnabled" region="DE" />
</q-layout>
</template>
@ -128,8 +130,7 @@
export default {
name: 'default',
mounted: function() {
this.$refs.layout.showLeft();
this.$refs.layout.hideRight();
this.applyLayout();
if(!this.hasUser) {
startLoading();
this.$store.dispatch('user/initUser').then(()=>{
@ -164,16 +165,14 @@
CscCall
},
computed: {
layout: {
get(){
return this.$store.state.layout.sides;
},
set(sides) {
this.$store.commit('layout/updateSides', sides);
}
},
...mapGetters('layout', [
'right',
'left',
'isFullscreenEnabled'
]),
...mapGetters('call', [
'isCallAvailable',
'isCalling',
'hasCallInitFailure'
]),
...mapGetters('user', [
@ -190,9 +189,29 @@
}),
hasCommunicationCapabilities() {
return this.isCallAvailable || this.hasSmsCapability || this.hasFaxCapability;
},
callClasses() {
let classes = {};
if(this.isFullscreenEnabled) {
classes['csc-call-fullscreen'] = true;
}
if(this.isCalling) {
classes['csc-call-calling'] = true;
}
return classes;
},
layoutView() {
if(this.isFullscreenEnabled) {
return 'lHr LpR lFr';
} else {
return 'lHh LpR lFf';
}
}
},
methods: {
toggleFullscreen() {
this.$store.commit('layout/toggleFullscreen');
},
showInitialToasts() {
if(this.isCallAvailable) {
showToast(this.$i18n.t('toasts.callAvailable'));
@ -203,7 +222,7 @@
}
},
call() {
this.$refs.layout.showRight();
this.$store.commit('layout/showRight');
this.$refs.cscCall.init();
},
logout() {
@ -212,6 +231,45 @@
stopLoading();
this.$router.push({path: '/login'});
})
},
rightBreakPoint() {
if(this.right) {
this.$store.commit('layout/showRight');
this.$store.commit('layout/hideLeft');
} else {
this.$store.commit('layout/hideRight');
}
},
closeCall() {
this.$store.commit('layout/hideRight');
},
applyLayout() {
if(this.right) {
this.$refs.layout.showRight();
} else {
this.$refs.layout.hideRight();
}
if(this.left) {
this.$refs.layout.showLeft();
} else {
this.$refs.layout.hideLeft();
}
}
},
watch: {
right(value) {
if(value) {
this.$refs.layout.showRight();
} else {
this.$refs.layout.hideRight();
}
},
left(value) {
if(value) {
this.$refs.layout.showLeft();
} else {
this.$refs.layout.hideLeft();
}
}
}
}
@ -283,6 +341,96 @@
z-index: 1001;
}
.layout-aside.fixed.csc-call-fullscreen {
left: 0;
right: 0;
top: 0;
bottom: 0;
width: auto;
z-index: 5000;
}
.csc-call-fullscreen .csc-call,
.csc-call-fullscreen .csc-call .q-card {
}
.csc-call-fullscreen .csc-call .q-card .q-card-primary {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 72px;
line-height: 72px;
z-index: 6001;
background: -moz-linear-gradient(top, rgba(51,64,77,1) 0%, rgba(235,236,237,0) 90%, rgba(255,255,255,0) 100%);
background: -webkit-linear-gradient(top, rgba(51,64,77,1) 0%,rgba(235,236,237,0) 90%,rgba(255,255,255,0) 100%);
background: linear-gradient(to bottom, rgba(51,64,77,1) 0%,rgba(235,236,237,0) 90%,rgba(255,255,255,0) 100%);
}
.csc-call-fullscreen .csc-call .q-card-actions {
position: absolute;
bottom: 0;
right: 0;
left: 0;
z-index: 6001;
}
.csc-call-fullscreen .csc-call .q-card-main {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 6000;
font-size: 0;
}
.csc-call-fullscreen .csc-call-media {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: 1;
}
.csc-media-remote {
z-index: 9;
}
.csc-call-fullscreen .csc-media-preview {
position: absolute;
bottom: 0;
left: 0;
width: 20%;
}
.csc-call-fullscreen .csc-media-preview video {
position: relative;
height: 100%;
}
.csc-call-fullscreen .csc-media-remote {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
.csc-call-fullscreen .csc-media-remote video {
position: absolute;
height: 100%;
bottom: 0;
}
.csc-call-fullscreen .csc-call-info {
position: relative;
top: 73px;
z-index: 2;
}
.q-if-control.q-if-control-before.q-icon,
.q-if-control.q-if-control-before.q-icon:before {
font-size:24px;

@ -150,10 +150,11 @@
},
"call": {
"startNew": "Start new call",
"initiating": "Call Initiating ...",
"ringing": "Call Ringing ...",
"ended": "Call ended",
"call": "Call",
"initiating": "Initiating ...",
"ringing": "Ringing ...",
"incoming": "Incoming ...",
"ended": "Ended",
"call": "Established",
"inputNumber": "Input a phone number",
"inputValidNumber": "Input a valid phone number",
"number": "Number",

@ -121,17 +121,19 @@ export class RtcEngineCall {
this.localMedia.setAudio(sources.defaultAudio);
}
if (hasVideo && _.isObject(sources.defaultVideo)) {
sources.defaultVideo.setQuality(cdk.MediaSourceQuality.HD);
this.localMedia.setVideo(sources.defaultVideo);
} else if (hasScreen && _.isObject(sources.desktopSharing)) {
sources.desktopSharing.setQuality(cdk.MediaSourceQuality.HD);
this.localMedia.setVideo(sources.desktopSharing);
}
});
this.localMedia.build((err)=>{
if(_.isObject(err)) {
reject(err);
} else {
resolve(this.localMedia);
}
this.localMedia.build((err)=>{
if(_.isObject(err)) {
reject(err);
} else {
resolve(this.localMedia);
}
});
});
});
}
@ -145,6 +147,8 @@ export class RtcEngineCall {
this.localCall.onEnded(()=>{
this.events.emit('ended', this.localCall.endedReason);
this.end();
}).onAccepted(()=>{
this.events.emit('accepted');
}).onPending(()=>{
this.events.emit('pending');
}).onRemoteMedia((remoteMediaStream)=>{
@ -192,6 +196,11 @@ export class RtcEngineCall {
return this;
}
onAccepted(listener) {
this.events.on('accepted', listener);
return this;
}
onPending(listener) {
this.events.on('pending', listener);
return this;
@ -227,6 +236,8 @@ export class RtcEngineCall {
this.remoteCall.accept({
localMediaStream: localMediaStream
});
} else {
throw new Error('Remote call does not exist');
}
}
@ -250,6 +261,56 @@ export class RtcEngineCall {
}
}
disableAudio() {
if(this.localCall !== null) {
this.localCall.disableAudio();
} else if (this.remoteCall !== null) {
this.remoteCall.disableAudio();
}
}
enableAudio() {
if(this.localCall !== null) {
this.localCall.enableAudio();
} else if (this.remoteCall !== null) {
this.remoteCall.enableAudio();
}
}
disableVideo() {
if(this.localCall !== null) {
this.localCall.disableVideo();
} else if (this.remoteCall !== null) {
this.remoteCall.disableVideo();
}
}
enableVideo() {
if(this.localCall !== null) {
this.localCall.enableVideo();
} else if (this.remoteCall !== null) {
this.remoteCall.enableVideo();
}
}
getCall() {
if(this.localCall !== null) {
return this.localCall;
} else if (this.remoteCall !== null) {
return this.remoteCall;
} else {
return null;
}
}
isRemoteSendingAudio() {
return this.getCall()._cdkCall.mediaInfo.remoteSdp.isOfferingAudio();
}
isRemoteSendingVideo() {
return this.getCall()._cdkCall.mediaInfo.remoteSdp.isOfferingVideo();
}
static getInstance() {
if(rtcEngineCallInstance === null) {
rtcEngineCallInstance = new RtcEngineCall();

@ -29,7 +29,10 @@ export default {
mediaType: null,
localMediaType: null,
localMediaStream: null,
remoteMediaStream: null
remoteMediaStream: null,
audioEnabled: true,
videoEnabled: true,
muted: false
},
getters: {
getNumber(state, getters) {
@ -85,6 +88,24 @@ export default {
},
hasRtcEngineCapabilityEnabled(state, getters, rootState, rootGetters) {
return rootGetters['user/hasRtcEngineCapabilityEnabled'];
},
hasRemoteVideo(state, getters) {
return state.remoteMediaStream !== null && state.remoteMediaStream.hasVideo();
},
hasLocalVideo(state, getters) {
return state.localMediaStream !== null && state.localMediaStream.hasVideo();
},
hasVideo(state, getters) {
return getters.hasLocalVideo || getters.hasRemoteVideo;
},
isAudioEnabled(state, getters) {
return state.audioEnabled;
},
isVideoEnabled(state, getters) {
return state.videoEnabled;
},
isMuted(state, getters) {
return state.muted;
}
},
mutations: {
@ -121,6 +142,7 @@ export default {
incomingCall(state, options) {
state.callState = CallState.incoming;
state.number = options.number;
state.mediaType = options.mediaType;
},
hangUpCall(state) {
state.callState = CallState.input;
@ -144,15 +166,41 @@ export default {
state.remoteMediaStream.stop();
state.remoteMediaStream = null;
}
},
disableAudio(state) {
state.audioEnabled = false;
},
enableAudio(state) {
state.audioEnabled = true;
},
disableVideo(state) {
state.videoEnabled = false;
},
enableVideo(state) {
state.videoEnabled = true;
},
mute(state) {
state.muted = true;
},
unmute(state) {
state.muted = false;
}
},
actions: {
initialize(context) {
return new Promise((resolve, reject)=>{
Vue.call.onIncoming(()=>{
let mediaType;
if(Vue.call.isRemoteSendingAudio()) {
mediaType = MediaType.audio;
}
if(Vue.call.isRemoteSendingVideo()) {
mediaType = MediaType.audioVideo;
}
context.commit('layout/showRight', null, { root: true });
context.commit('incomingCall', {
number: Vue.call.getNumber()
number: Vue.call.getNumber(),
mediaType: mediaType
});
}).onRemoteMedia((remoteMediaStream)=>{
context.commit('establishCall', remoteMediaStream);
@ -176,12 +224,6 @@ export default {
}
});
},
/**
* @param context
* @param options
* @param options.localMedia
* @param options.number
*/
start(context, options) {
context.commit('layout/showRight', null, { root: true });
context.commit('startCalling', {
@ -198,6 +240,7 @@ export default {
}).start(options.number, localMediaStream);
}).catch((err)=>{
context.commit('endCall', err.name);
console.error(err);
Vue.call.end();
});
},
@ -213,6 +256,22 @@ export default {
hangUp(context) {
Vue.call.hangUp();
context.commit('hangUpCall');
},
disableAudio(context) {
Vue.call.disableAudio();
context.commit('disableAudio');
},
enableAudio(context) {
Vue.call.enableAudio();
context.commit('enableAudio');
},
disableVideo(context) {
Vue.call.disableVideo();
context.commit('disableVideo');
},
enableVideo(context) {
Vue.call.enableVideo();
context.commit('enableVideo');
}
}
};

@ -6,7 +6,8 @@ export default {
sides: {
left: true,
right: false
}
},
fullscreenEnabled: false
},
getters: {
right(state) {
@ -14,13 +15,16 @@ export default {
},
left(state) {
return state.sides.left;
},
isFullscreenEnabled(state) {
return state.fullscreenEnabled;
}
},
mutations: {
updateSides(state, sides) {
state.sides = sides;
},
showRight(state){
showRight(state) {
state.sides.right = true;
},
hideRight(state){
@ -31,6 +35,19 @@ export default {
},
hideLeft(state){
state.sides.left = false;
},
toggleFullscreen(state) {
if(state.fullscreenEnabled) {
state.fullscreenEnabled = false;
} else {
state.fullscreenEnabled = true;
}
},
enableFullscreen(state) {
state.fullscreenEnabled = true;
},
disableFullscreen(state) {
state.fullscreenEnabled = false;
}
},
actions: {}

Loading…
Cancel
Save