TT#31302 Conferencing: As a Customer, I want to join a conference by using a URL

Change-Id: I0186ae80529dfaf35d08a22deb4f9b249bc16279
changes/55/28255/8
Hans-Peter Herzog 6 years ago
parent 8d95437a08
commit f1f5bc3583

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

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

@ -0,0 +1,205 @@
<template>
<q-layout>
<div
id="csc-conf-header"
class="csc-conf-full-height"
>
<div
class="row justify-end"
>
<q-btn
class="csc-conf-button"
color="primary"
icon="clear"
flat
round
@click="close()"
/>
</div>
</div>
<div
id="csc-conf-body"
>
<csc-conference-join
id="csc-conf-join"
v-if="!isJoined"
:conferenceId="conferenceId"
:local-media-stream="localMediaStream"
:is-microphone-enabled="isMicrophoneEnabled"
:is-camera-enabled="isCameraEnabled"
:is-screen-enabled="isScreenEnabled"
:is-media-enabled="isMediaEnabled"
/>
<csc-conference-joined
v-else
>
</csc-conference-joined>
</div>
<div
id="csc-conf-main-media"
v-show="isMediaEnabled && isCameraEnabled"
>
<csc-media
ref="localMedia"
:muted="true"
:stream="localMediaStream"
:preview="false"
/>
</div>
<div
id="csc-conf-footer"
>
<div
id="csc-conf-actions"
class="row justify-center"
>
<q-btn
class="csc-conf-button"
:color="microphoneButtonColor"
icon="mic"
round
@click="toggleMicrophone()"
/>
<q-btn
class="csc-conf-button"
:color="cameraButtonColor"
icon="videocam"
round
@click="toggleCamera()"
/>
<q-btn
class="csc-conf-button"
:color="screenButtonColor"
icon="computer"
round
@click="toggleScreen()"
/>
</div>
</div>
</q-layout>
</template>
<script>
import {
mapGetters
} from 'vuex'
import CscConferenceJoin from '../pages/Conference/CscConferenceJoin'
import CscConferenceJoined from '../pages/Conference/CscConferenceJoined'
import CscMedia from "../CscMedia";
import {
QLayout,
QBtn
} from 'quasar-framework'
export default {
data () {
return {}
},
mounted() {
this.$store.dispatch('user/initUser');
},
components: {
CscMedia,
CscConferenceJoin,
CscConferenceJoined,
QLayout,
QBtn
},
computed: {
...mapGetters([
'conferenceId'
]),
...mapGetters('conference', [
'isConferencingEnabled',
'isJoined',
'isMicrophoneEnabled',
'isCameraEnabled',
'isScreenEnabled',
'isMediaEnabled',
'localMediaStream'
]),
microphoneButtonColor() {
if(this.isMicrophoneEnabled) {
return 'primary';
}
else {
return 'grey';
}
},
cameraButtonColor() {
if(this.isCameraEnabled) {
return 'primary';
}
else {
return 'grey';
}
},
screenButtonColor() {
if(this.isScreenEnabled) {
return 'primary';
}
else {
return 'grey';
}
}
},
methods: {
close() {
this.$router.push({path: '/user/home'});
this.$store.commit('conference/disposeLocalMedia');
},
toggleMicrophone() {
this.$store.dispatch('conference/toggleMicrophone');
},
toggleCamera() {
this.$store.dispatch('conference/toggleCamera');
},
toggleScreen() {
this.$store.dispatch('conference/toggleScreen');
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../themes/app.common.styl'
#csc-conf-main-media
position absolute
top 0
bottom 0
right 0
left 0
z-index 1
background-color black
font-size 0
#csc-conf-header
z-index 2
top 0
left 0
right 0
position fixed
background-color transparent
height $call-footer-height
.csc-conf-button.q-btn
.q-btn-inner
color white
#csc-conf-body
position relative
z-index 2
top $call-footer-height
#csc-conf-footer
z-index 2
bottom 0
left 0
right 0
position fixed
background-color $layout-aside-background
height $call-footer-height
#csc-conf-actions
margin: -28px
.q-btn:last-child
margin-right 0
.q-btn
margin-right $flex-gutter-sm
</style>

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

@ -0,0 +1,95 @@
<template>
<div
class="row justify-center items-center csc-conf-full-height"
>
<div
id="csc-conf-join-content"
class="col col-4 text-center"
>
<p
id="csc-conf-join-text"
>{{ $t('conferencing.joinText') }}</p>
<q-input
ref="conferenceName"
id="csc-conf-link-input"
dark
:value="conferenceId"
align="center"
readonly
:after="conferenceNameButtons"
/>
<q-btn
class="csc-button"
color="primary"
icon="call"
round
/>
</div>
</div>
</template>
<script>
import CscMedia from '../../CscMedia'
import {
QBtn,
QInput
} from 'quasar-framework'
export default {
name: 'csc-conference-join',
data () {
return {}
},
props: [
'conferenceId',
'localMediaStream',
'isMicrophoneEnabled',
'isCameraEnabled',
'isScreenEnabled',
'isMediaEnabled'
],
components: {
QBtn,
QInput,
CscMedia
},
computed: {
conferenceNameButtons() {
let buttons = [];
let self = this;
buttons.push({
icon: 'link',
error: false,
handler (event) {
event.stopPropagation();
self.copyLinkToClipboard();
}
}
);
return buttons;
},
conferenceLinkValue() {
return window.location.href;
}
},
watch: {
},
methods:{
copyLinkToClipboard() {
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@import '../../../themes/app.common.styl'
#csc-conf-link-input
margin-bottom $flex-gutter-md
#csc-conf-join-text
margin-bottom $flex-gutter-md
font-weight bold
font-size 1rem
#csc-conf-join-content
position relative
z-index 2
</style>

@ -0,0 +1,15 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'csc-conference-joined',
data () {
return {}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
</style>

@ -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') {

@ -53,11 +53,11 @@
:dark="true"
:value="callNumberInput"
:readonly="dialpadOpened"
:enabled="isCallInitialized"
:enabled="isCallEnabled"
@number-changed="numberInputChanged"
/>
<csc-call-dialpad
v-if="dialpadOpened && isCallInitialized"
v-if="dialpadOpened && isCallEnabled"
:show-backspace-button="true"
:show-clear-button="true"
@click="dialpadClick"
@ -131,7 +131,7 @@
'callNumberInput',
'hasRtcEngineCapabilityEnabled',
'desktopSharingInstall',
'isCallInitialized',
'isCallEnabled',
'isCallInitializing'
]),
dialpadOpened() {

@ -34,6 +34,7 @@
"toasts": {
"callAvailable": "You are now able to start and receive calls",
"callNotAvailable": "Could not initialize call functionality properly",
"conferencingAvailable": "You are now able to create WebRTC multiparty conferences",
"changeSessionLanguageSuccessMessage": "Session language successfully changed"
},
"validationErrors": {
@ -518,5 +519,8 @@
"deleteCustomDialogTitle": "Reset busy greeting sound",
"deleteCustomDialogText": "You are about to reset the custom {type} greeting sound to defaults",
"unavailable": "unavailable"
},
"conferencing": {
"joinText": "Join conference with name"
}
}

@ -17,9 +17,6 @@ import router from './router'
import {
sync
} from 'vuex-router-sync'
import {
RtcEngineCall
} from './plugins/call'
import App from './App.vue'
import './filters'
import VueScrollTo from 'vue-scrollto'
@ -34,7 +31,6 @@ Platform.has.popstate = false;
Vue.config.productionTip = false;
Vue.use(Quasar);
Vue.use(VueResource);
Vue.use(RtcEngineCall);
Vue.use(VueScrollTo);
Vue.use(Vuelidate);

@ -45,10 +45,11 @@ let rtcEngineCallInstance = null;
export class RtcEngineCall {
constructor() {
this.networkTag = 'sip';
// this.networkTag = 'sip';
this.network = null;
this.loadedLibrary = null;
this.sessionToken = null;
// this.loadedLibrary = null;
// this.sessionToken = null;
this.rtcEngine = null;
this.localMedia = null;
this.remoteMedia = null;
this.currentCall = null;
@ -56,17 +57,11 @@ export class RtcEngineCall {
this.endedReason = null;
}
initialize() {
return new Promise((resolve, reject)=>{
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);
}
}

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

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

@ -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: '*',

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

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

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

@ -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)=>{

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

Loading…
Cancel
Save