TT#142700 CSC: Create a PoC integration of the VoIP(NGCP) call using Javascript library JsSip in combination with Kamailio's WebSocket module

- Enable and disable camera during the call
- Enable and disable screen during the call
- Switch from camera to screen and back
- Send in-band DTMF
- Send "603 Decline" on termination

Change-Id: Ife56ca49cadade44ee9b70b77b3f345b262be9d9
pull/9/head
Hans-Peter Herzog 4 years ago
parent 3d1c2a7a6a
commit 238f78cb05

2
env/Dockerfile vendored

@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-buster:latest
# is updated with the current date. It will force refresh of all
# of the base images and things like `apt-get update` won't be using
# old cached versions when the Dockerfile is built.
ENV REFRESHED_AT 2021-07-07
ENV REFRESHED_AT 2021-09-27
# files that get-code generates
COPY env/sources.list.d/builddeps.list /etc/apt/sources.list.d/

@ -31,6 +31,7 @@
"core-js": "^3.6.5",
"file-saver": "^2.0.2",
"jest-junit": "^11.1.0",
"jssip": "3.8.2",
"jwt-decode": "^2.2.0",
"load-script": "^1.0.0",
"lodash": "4.17.21",

@ -27,11 +27,11 @@ module.exports = function (ctx) {
// https://quasar.dev/quasar-cli/boot-files
boot: [
'appConfig',
'rtc-engine',
'filters',
'vuelidate',
'i18n',
'vue-resource',
'ngcp-call',
'user',
'routes',
'vue-scrollto',

@ -0,0 +1,298 @@
import {
EventEmitter
} from 'events'
import jssip from 'jssip'
let $baseWebSocketUrl = null
let $subscriber = null
let $socket = null
let $userAgent = null
let $outgoingRtcSession = null
let $incomingRtcSession = null
let $localMediaStream = null
let $remoteMediaStream = null
let $isVideoScreen = false
const TERMINATION_OPTIONS = {
status_code: 603,
reason_phrase: 'Decline'
}
export const callEvent = new EventEmitter()
function handleRemoteMediaStream (trackEvent) {
const stream = trackEvent.streams[0]
if (!$remoteMediaStream) {
$remoteMediaStream = stream
callEvent.emit('remoteStream', $remoteMediaStream)
} else if ($remoteMediaStream && $remoteMediaStream.id !== stream.id) {
$remoteMediaStream = stream
callEvent.emit('remoteStream', $remoteMediaStream)
}
}
function getSubscriberUri () {
return 'sip:' + $subscriber.username + '@' + $subscriber.domain
}
function callCreateSocket () {
return new jssip.WebSocketInterface($baseWebSocketUrl + '/' + $subscriber.username)
}
export function callConfigure ({ baseWebSocketUrl }) {
$baseWebSocketUrl = baseWebSocketUrl
}
export async function callInitialize ({ subscriber }) {
$subscriber = subscriber
callRegister()
}
export function callRegister () {
if (!$socket) {
$socket = callCreateSocket()
const config = {
sockets: [$socket],
uri: getSubscriberUri(),
password: $subscriber.password
}
$userAgent = new jssip.UA(config)
const delegateEvent = (eventName) => {
$userAgent.on(eventName, (event) => callEvent.emit(eventName, event))
}
delegateEvent('connected')
delegateEvent('disconnected')
delegateEvent('newRTCSession')
delegateEvent('newMessage')
delegateEvent('registered')
delegateEvent('unregistered')
delegateEvent('registrationFailed')
$userAgent.on('newRTCSession', (event) => {
if (event.originator === 'remote') {
$incomingRtcSession = event.session
$incomingRtcSession.on('peerconnection', () => {
$incomingRtcSession.connection.ontrack = handleRemoteMediaStream
})
$incomingRtcSession.on('failed', (failedEvent) => {
callEvent.emit('incomingFailed', failedEvent)
})
$incomingRtcSession.on('ended', (failedEvent) => {
callEvent.emit('incomingEnded', failedEvent)
$incomingRtcSession = null
})
callEvent.emit('incoming', $incomingRtcSession)
}
})
$userAgent.start()
}
}
export function callUnregister () {
if ($userAgent) {
$userAgent.unregister({
all: true
})
}
}
export async function callStart ({ number }) {
$localMediaStream = await callCreateLocalAudioStream()
callEvent.emit('localStream', $localMediaStream)
$outgoingRtcSession = $userAgent.call(number, {
eventHandlers: {
progress (event) {
callEvent.emit('outgoingProgress', event)
},
failed (event) {
callEvent.emit('outgoingFailed', event)
},
confirmed (event) {
callEvent.emit('outgoingConfirmed', event)
},
ended (event) {
callEvent.emit('outgoingEnded', event)
$outgoingRtcSession = null
}
},
mediaStream: $localMediaStream
})
$outgoingRtcSession.connection.ontrack = handleRemoteMediaStream
const delegateEvent = (eventName, newName) => {
$outgoingRtcSession.on(eventName, (event) => callEvent.emit(newName, event))
}
delegateEvent('failed', 'outgoingFailed')
}
export async function callAccept () {
$localMediaStream = await callCreateLocalAudioStream()
callEvent.emit('localStream', $localMediaStream)
if ($incomingRtcSession) {
$incomingRtcSession.answer({
mediaStream: $localMediaStream
})
}
}
export async function callCreateLocalAudioStream () {
return await navigator.mediaDevices.getUserMedia({
video: false,
audio: true
})
}
export function callGetRemoteMediaStream () {
return $remoteMediaStream
}
export function callGetRemoteMediaStreamId () {
return $remoteMediaStream?.id
}
export function callGetLocalMediaStream () {
return $localMediaStream
}
export function callGetLocalMediaStreamId () {
return $localMediaStream?.id
}
export function callGetRtcSession () {
if ($outgoingRtcSession) {
return $outgoingRtcSession
} else if ($incomingRtcSession) {
return $incomingRtcSession
}
}
export async function callChangeVideoStream (stream) {
if ($localMediaStream && callGetRtcSession()) {
callGetRtcSession().connection.getSenders().forEach(
sender => callGetRtcSession().connection.removeTrack(sender)
)
$localMediaStream.getTracks().forEach(track => track.stop())
$localMediaStream = stream
$localMediaStream.getTracks().forEach(
track => callGetRtcSession().connection.addTrack(track, $localMediaStream)
)
await callRenegotiate()
}
}
export async function callAddCamera () {
await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({
video: {
width: {
ideal: 4096
},
height: {
ideal: 2160
}
},
audio: true
}))
$isVideoScreen = false
}
export async function callAddScreen () {
$isVideoScreen = true
await callChangeVideoStream(await navigator.mediaDevices.getDisplayMedia({
video: {
width: {
ideal: 4096
},
height: {
ideal: 2160
}
},
audio: true
}))
}
export async function callRemoveVideo () {
$isVideoScreen = false
await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({
video: false,
audio: true
}))
}
export async function callRenegotiate () {
callGetRtcSession().renegotiate({
useUpdate: false
}, () => {
callEvent.emit('renegotiationSucceeded')
})
}
export function callHasRemoteVideo () {
return $remoteMediaStream?.getVideoTracks?.()?.length > 0
}
export function callHasLocalVideo () {
return $localMediaStream?.getVideoTracks?.()?.length > 0
}
export function callHasLocalCamera () {
return callHasLocalVideo() && !$isVideoScreen
}
export function callHasLocalScreen () {
return callHasLocalVideo() && $isVideoScreen
}
export function callToggleMicrophone () {
const config = {
audio: true,
video: false
}
const rtcSession = callGetRtcSession()
const muted = rtcSession?.isMuted()
if (muted.audio) {
rtcSession.unmute(config)
} else {
rtcSession.mute(config)
}
}
export function callIsMuted () {
return callGetRtcSession()?.isMuted()?.audio
}
export function callSendDTMF (tone, transport = 'RFC2833') {
const rtcSession = callGetRtcSession()
if (rtcSession) {
rtcSession.sendDTMF(tone, {
transportType: transport
})
}
}
export function callToggleRemoteAudio () {
if ($remoteMediaStream && $remoteMediaStream.getAudioTracks()[0]) {
$remoteMediaStream.getAudioTracks()[0].enabled = !$remoteMediaStream?.getAudioTracks()[0]?.enabled
}
}
export function callIsRemoteAudioMuted () {
return !$remoteMediaStream?.getAudioTracks()[0]?.enabled
}
export function callEnd () {
if ($outgoingRtcSession && !$outgoingRtcSession.isEnded()) {
$outgoingRtcSession.terminate(TERMINATION_OPTIONS)
$outgoingRtcSession = null
}
if ($incomingRtcSession && !$incomingRtcSession.isEnded()) {
$incomingRtcSession.terminate(TERMINATION_OPTIONS)
$incomingRtcSession = null
}
if ($localMediaStream) {
$localMediaStream.getTracks().forEach(track => track.stop())
$localMediaStream = null
}
if ($remoteMediaStream) {
$remoteMediaStream.getTracks().forEach(track => track.stop())
$remoteMediaStream = null
}
}

@ -0,0 +1,67 @@
import {
v4
} from 'uuid'
import _ from 'lodash'
import {
callConfigure,
callEnd,
callEvent,
callHasLocalCamera,
callHasLocalScreen,
callHasRemoteVideo,
callIsMuted,
callIsRemoteAudioMuted
} from 'src/api/ngcp-call'
import { errorVisibilityTimeout } from 'src/store/call/common'
export default async ({ Vue, app, store }) => {
callConfigure({
baseWebSocketUrl: app.$appConfig.baseWsUrl + '/wss/sip'
})
const callFailed = (event) => {
let cause = event.cause
const reason = event.message?.parseHeader?.('Reason')
if (reason?.text) {
cause = reason.text
}
if (event.originator !== 'local') {
callEnd()
store.commit('call/endCall', cause)
setTimeout(() => {
store.commit('call/inputNumber')
}, errorVisibilityTimeout)
}
}
callEvent.on('connected', () => {
store.commit('call/enableCall')
})
callEvent.on('disconnected', () => {
store.commit('call/disableCall')
})
callEvent.on('outgoingProgress', (event) => {
store.commit('call/startRinging')
})
callEvent.on('outgoingFailed', callFailed)
callEvent.on('incomingFailed', callFailed)
callEvent.on('outgoingEnded', callFailed)
callEvent.on('incomingEnded', callFailed)
callEvent.on('incoming', (session) => {
store.commit('call/incomingCall', {
number: _.get(session, 'remote_identity.uri.user', 'Unknown')
})
})
callEvent.on('localStream', () => {
store.commit('call/toggleMicrophone', !callIsMuted())
})
callEvent.on('remoteStream', () => {
store.commit('call/establishCall', {
mediaStreamId: v4(),
isLocalAudioMuted: callIsMuted(),
hasLocalCamera: callHasLocalCamera(),
hasLocalScreen: callHasLocalScreen(),
hasRemoteVideo: callHasRemoteVideo(),
isRemoteAudioMuted: callIsRemoteAudioMuted()
})
})
}

@ -1,76 +0,0 @@
import Vue from 'vue'
import getRtcEnginePlugin from 'src/plugins/rtc-engine'
import CallPlugin from 'src/plugins/call'
import ConferencePlugin from 'src/plugins/conference'
import { errorVisibilityTimeout } from 'src/store/call'
export default ({ Vue, store, app }) => {
const rtcPluginConfig = {
cdkScriptUrl: app.$appConfig.baseHttpUrl + '/rtc/files/dist/cdk-prod.js',
webSocketUrl: app.$appConfig.baseWsUrl + '/rtc/api',
ngcpApiBaseUrl: app.$appConfig.baseHttpUrl
// ngcpApiJwt: ... // Note: this value will be set in userInit action, with value from "getJwt" function
}
const RtcEnginePlugin = getRtcEnginePlugin(rtcPluginConfig)
Vue.use(RtcEnginePlugin)
Vue.use(CallPlugin)
Vue.use(ConferencePlugin)
rtcEngine(store)
call(store)
conference(store)
}
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')
})
}
function call (store) {
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)
})
}
function conference (store) {
Vue.$conference.onConferenceParticipantJoined((participant) => {
store.commit('conference/participantJoined', participant)
participant.onMediaStream(() => {
store.commit('conference/removeRemoteMedia', participant.id)
store.commit('conference/addRemoteMedia', participant.id)
}).onMediaEnded(() => {
store.commit('conference/removeRemoteMedia', participant.id)
})
}).onConferenceParticipantLeft((participant) => {
store.commit('conference/participantLeft', participant)
store.commit('conference/removeRemoteMedia', participant.id)
store.commit('conference/setSelectedParticipant', participant.id)
}).onConferenceEvent((event) => {
store.commit('conference/event', event)
}).onConferenceMessage((message) => {
store.commit('conference/message', message)
}).onConferenceFile((file) => {
store.commit('conference/file', file)
}).onLocalMediaStreamEnded(() => {
store.commit('conference/disposeLocalMedia')
}).onConferenceEnded(() => {
store.dispatch('conference/leave')
})
}

@ -68,13 +68,7 @@ export default {
icon: 'call',
label: this.callStateTitle,
sublabel: this.callStateSubtitle,
visible: this.isRtcEngineUiVisible
},
{
to: '/conference',
icon: 'videocam',
label: this.$t('Join conference'),
visible: this.isRtcEngineUiVisible
visible: true
},
{
to: '/user/conversations',

@ -170,7 +170,7 @@
@click="toggleMicrophone()"
/>
<q-btn
v-if="isEstablished && hasLocalVideo && !(isMobile && minimized)"
v-if="isEstablished && !(isMobile && minimized)"
:color="colorToggleCamera"
:icon="iconToggleCamera"
class="q-mr-sm"
@ -179,6 +179,16 @@
size="large"
@click="toggleCamera()"
/>
<q-btn
v-if="isEstablished && !(isMobile && minimized)"
:color="colorToggleScreen"
icon="screen_share"
class="q-mr-sm"
text-color="dark"
round
size="large"
@click="toggleScreen()"
/>
<q-btn
v-if="isEstablished && !(isMobile && minimized)"
:color="colorToggleRemoteVolume"
@ -209,34 +219,16 @@
size="large"
@click="closeCall()"
/>
<q-fab
<q-btn
v-if="canStart"
ref="startButton"
color="primary"
text-color="dark"
icon="call"
direction="up"
>
<q-fab-action
v-if="!isMobile"
color="primary"
text-color="dark"
icon="computer"
@click="startCall('audioScreen')"
/>
<q-fab-action
color="primary"
text-color="dark"
icon="videocam"
@click="startCall('audioVideo')"
/>
<q-fab-action
color="primary"
text-color="dark"
icon="call"
class="q-mr-sm"
round
size="large"
@click="startCall('audioOnly')"
/>
</q-fab>
</div>
<div
v-if="minimized"
@ -343,7 +335,7 @@ import {
} from 'src/helpers/ui'
import CscMedia from '../CscMedia'
import CscCallDialpad from '../CscCallDialpad'
import { CallStateTitle } from 'src/store/call'
import { CallStateTitle } from 'src/store/call/common'
export default {
name: 'CscCall',
components: {
@ -406,6 +398,10 @@ export default {
type: Boolean,
default: false
},
screenEnabled: {
type: Boolean,
default: false
},
remoteVolumeEnabled: {
type: Boolean,
default: false
@ -494,7 +490,7 @@ export default {
if (this.microphoneEnabled) {
return 'primary'
} else {
return 'faded'
return 'grey-1'
}
},
iconToggleCamera () {
@ -508,7 +504,14 @@ export default {
if (this.cameraEnabled) {
return 'primary'
} else {
return 'faded'
return 'grey-1'
}
},
colorToggleScreen () {
if (this.screenEnabled) {
return 'primary'
} else {
return 'grey-1'
}
},
iconToggleRemoteVolume () {
@ -522,7 +525,7 @@ export default {
if (this.remoteVolumeEnabled) {
return 'primary'
} else {
return 'faded'
return 'grey-1'
}
},
callStateTitle () {
@ -604,9 +607,6 @@ export default {
toggleMicrophone () {
this.$emit('toggle-microphone')
},
toggleCamera () {
this.$emit('toggle-camera')
},
toggleRemoteVolume () {
this.$emit('toggle-remote-volume')
},
@ -635,6 +635,12 @@ export default {
this.$refs.remoteMedia.fitMedia()
}
})
},
toggleCamera () {
this.$emit('toggle-camera')
},
toggleScreen () {
this.$emit('toggle-screen')
}
}
}

@ -172,6 +172,7 @@
:has-remote-video="hasRemoteVideo"
:microphone-enabled="isMicrophoneEnabled"
:camera-enabled="isCameraEnabled"
:screen-enabled="isScreenEnabled"
:remote-volume-enabled="isRemoteVolumeEnabled"
:dialpad-opened="isDialpadOpened"
:menu-minimized="menuMinimized"
@ -181,11 +182,13 @@
@close-call="closeCall"
@toggle-microphone="toggleMicrophone"
@toggle-camera="toggleCamera"
@toggle-remote-volume="toggleRemoteVolume"
@toggle-screen="toggleScreen"
@toggle-remote-volume="toggleRemoteAudio"
@click-dialpad="clickDialpad"
@toggle-dialpad="toggleDialpad"
@maximize-call="maximizeCall"
@minimize-call="minimizeCall"
@add-camera="toggleCamera"
/>
</q-layout>
</template>
@ -275,6 +278,7 @@ export default {
'hasRemoteVideo',
'isMicrophoneEnabled',
'isCameraEnabled',
'isScreenEnabled',
'isRemoteVolumeEnabled',
'isMaximized',
'isDialpadOpened',
@ -435,6 +439,12 @@ export default {
'forwardHome',
'fetchAuthToken'
]),
...mapActions('call', [
'toggleCamera',
'toggleScreen',
'toggleMicrophone',
'toggleRemoteAudio'
]),
layoutResized () {
this.$refs.call.fitMedia()
},
@ -476,15 +486,6 @@ export default {
endCall () {
this.$store.dispatch('call/end')
},
toggleMicrophone () {
this.$store.dispatch('call/toggleMicrophone')
},
toggleCamera () {
this.$store.dispatch('call/toggleCamera')
},
toggleRemoteVolume () {
this.$store.dispatch('call/toggleRemoteVolume')
},
clickDialpad (value) {
this.$store.dispatch('call/sendDTMF', value)
},

@ -6,19 +6,11 @@
<div
class="col-xs-10 col-sm-8 col-md-4 csc-opt-center"
>
<csc-inline-alert-info
v-if="!isCallInitializing && !hasRtcEngineCapabilityEnabled"
class="q-mb-lg"
>
{{ $t('You can neither make a call nor receive one, since the RTC:engine is not active. If you operate a C5 CE then first upgrade to a C5 PRO to be able to use the RTC:engine.') }}
</csc-inline-alert-info>
<csc-input
id="csc-call-number-input"
:label="$t('Enter a number to dial')"
:value="callNumberInput"
:readonly="dialpadOpened"
:disable="!isCallEnabled"
:loading="isCallInitializing"
clearable
@keypress.space.prevent
@keydown.space.prevent
@ -54,7 +46,6 @@ import {
} from 'vuex'
import CscCallDialpad from 'components/CscCallDialpad'
import CscPage from 'components/CscPage'
import CscInlineAlertInfo from 'components/CscInlineAlertInfo'
import CscInput from 'components/form/CscInput'
export default {
@ -66,7 +57,6 @@ export default {
},
components: {
CscInput,
CscInlineAlertInfo,
CscPage,
CscCallDialpad
},
@ -83,18 +73,12 @@ export default {
...mapGetters('call', [
'callState',
'callNumberInput',
'hasRtcEngineCapabilityEnabled',
'desktopSharingInstall',
'isCallEnabled',
'isCallInitializing',
'callStateTitle',
'callStateSubtitle'
]),
dialpadOpened () {
return this.callState === 'input' &&
!this.isCallInitializing &&
this.isMobile &&
this.hasRtcEngineCapabilityEnabled
return this.callState === 'input' && this.isMobile
},
pageClasses () {
const classes = ['row', 'justify-center']
@ -131,7 +115,7 @@ export default {
this.$store.commit('call/numberInputChanged', '')
},
startCall () {
if (this.callNumberInput !== '' && this.callNumberInput !== null) {
if (this.callNumberInput && this.callNumberInput !== '') {
this.$store.dispatch('call/start', 'audioOnly')
}
}

@ -1,327 +0,0 @@
import EventEmitter from 'events'
export const LocalMedia = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
videoOnly: 'videoOnly',
audioScreen: 'audioScreen',
screenOnly: 'screenOnly'
}
export class NetworkNotConnected {
constructor (network) {
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'
}
}
let rtcEngineCallInstance = null
export class RtcEngineCall {
constructor () {
this.network = null
this.rtcEngine = null
this.localMedia = null
this.remoteMedia = null
this.currentCall = null
this.events = new EventEmitter()
this.endedReason = null
}
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) {
this.currentCall = remoteCall
this.currentCall.onEnded(() => {
this.events.emit('ended', this.currentCall.endedReason)
}).onRemoteMedia((remoteMediaStream) => {
this.remoteMedia = remoteMediaStream
this.events.emit('remoteMedia', remoteMediaStream)
}).onRemoteMediaEnded(() => {
this.events.emit('remoteMediaEnded')
}).onError((err) => {
console.debug(err)
this.end()
this.events.emit('ended', err.message)
})
}
this.events.emit('incoming')
})
}).onSipNetworkDisconnected(() => {
this.events.emit('disconnected')
})
}
}
isAvailable () {
return this.network !== null && this.network.isConnected
}
hasRunningCall () {
return this.currentCall !== null
}
createLocalMedia (localMedia) {
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-undef
const localMediaBuilder = cdk.media.create()
if (localMedia === LocalMedia.audioOnly || localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.audioScreen) {
localMediaBuilder.enableMicrophone()
}
if (localMedia === LocalMedia.audioVideo || localMedia === LocalMedia.videoOnly) {
localMediaBuilder.enableCamera()
} else if (localMedia === LocalMedia.audioScreen || localMedia === LocalMedia.screenOnly) {
localMediaBuilder.enableScreen()
}
localMediaBuilder.build().then((localMediaStream) => {
this.localMedia = localMediaStream
resolve(this.localMedia)
}).catch((err) => {
reject(err)
})
})
}
start (peer, localMediaStream) {
if (this.network !== null && this.currentCall === null) {
peer = peer.replace(/(\s|\+)/, '')
this.currentCall = this.network.call(peer, {
localMediaStream: localMediaStream
})
this.currentCall.onEnded(() => {
this.events.emit('ended', this.currentCall.endedReason)
this.end()
}).onRemoteMedia((remoteMediaStream) => {
this.remoteMedia = remoteMediaStream
this.events.emit('remoteMedia', remoteMediaStream)
}).onRemoteMediaEnded(() => {
this.events.emit('remoteMediaEnded')
}).onAccepted(() => {
this.events.emit('accepted')
}).onPending(() => {
this.events.emit('pending')
}).onRingingStart(() => {
this.events.emit('ringingStart')
}).onRingingStop(() => {
this.events.emit('ringingStop')
}).onError((err) => {
console.debug(err)
this.end()
this.events.emit('ended', err.message)
})
} else if (this.network !== null) {
throw new CallAlreadyExists()
} else {
throw new NetworkNotConnected(this.networkTag) // TODO: "this.networkTag" is not defined. We should get this variable from somewhere
}
}
getNumber () {
if (this.currentCall !== null) {
return this.currentCall.peer
} else {
return null
}
}
onIncoming (listener) {
this.events.on('incoming', listener)
return this
}
onAccepted (listener) {
this.events.on('accepted', 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
}
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(() => {
this.events.emit('locallyAccepted')
}).catch((err) => {
console.debug(err)
this.end()
this.events.emit('ended', err.message)
})
} else {
throw new Error('Remote call does not exist')
}
}
hangUp () {
this.end()
}
end () {
if (this.currentCall !== null) {
this.currentCall.end()
this.currentCall = null
}
this.endMedia()
}
endMedia () {
if (this.localMedia !== null) {
this.localMedia.stop()
this.localMedia = null
}
if (this.remoteMedia !== null) {
this.remoteMedia.stop()
this.remoteMedia = null
}
}
disableAudio () {
if (this.currentCall !== null) {
this.currentCall.disableAudio()
}
}
enableAudio () {
if (this.currentCall !== null) {
this.currentCall.enableAudio()
}
}
disableVideo () {
if (this.currentCall !== null) {
this.currentCall.disableVideo()
}
}
enableVideo () {
if (this.currentCall !== null) {
this.currentCall.enableVideo()
}
}
sendDTMF (char) {
if (this.currentCall !== null) {
this.currentCall.sendDTMF(char)
}
}
getCall () {
if (this.currentCall !== null) {
return this.currentCall
} else {
return null
}
}
getLocalMediaId () {
if (this.localMedia !== null) {
return this.localMedia.getStream().id
}
return null
}
getLocalMediaStream () {
if (this.localMedia !== null) {
return this.localMedia.getStream()
}
return null
}
getRemoteMediaId () {
if (this.remoteMedia !== null) {
return this.remoteMedia.getStream().id
}
return null
}
getRemoteMediaStream () {
if (this.remoteMedia !== null) {
return this.remoteMedia.getStream()
}
return null
}
hasRemoteVideo () {
if (this.remoteMedia !== null) {
return this.remoteMedia.hasVideo()
}
return false
}
hasLocalVideo () {
if (this.localMedia !== null) {
return this.localMedia.hasVideo()
}
return false
}
static getInstance () {
if (rtcEngineCallInstance === null) {
rtcEngineCallInstance = new RtcEngineCall()
}
return rtcEngineCallInstance
}
}
export default {
install (Vue) {
Vue.$call = RtcEngineCall.getInstance()
Vue.$call.setRtcEngine(Vue.$rtcEngine)
}
}

@ -1,163 +0,0 @@
import EventEmitter from 'events'
let conferencePlugin = null
export class ConferencePlugin {
constructor () {
this.events = new EventEmitter()
this.rtcEngine = null
this.conference = null
this.localMediaStream = null
}
setRtcEngine (rtcEngine) {
if (this.rtcEngine === null) {
this.rtcEngine = rtcEngine
this.rtcEngine.onConferenceNetworkConnected((network) => {
this.events.emit('connected')
network
.onConferenceParticipantJoined((participant) => {
this.events.emit('participantJoined', participant)
})
.onConferenceParticipantLeft((participant) => {
this.events.emit('participantLeft', participant)
})
.onConferenceEvent((event) => {
this.events.emit('conferenceEvent', event)
})
.onConferenceMessage((message) => {
this.events.emit('conferenceMessage', message)
})
.onConferenceFile((file) => {
this.events.emit('conferenceFile', file)
})
.onConferenceEnded(() => {
this.events.emit('conferenceEnded')
})
}).onConferenceNetworkDisconnected(() => {
this.events.emit('disconnected')
})
}
}
getNetwork () {
return this.rtcEngine.getConferenceNetwork()
}
async joinConference (options) {
options.localMediaStream = this.getLocalMediaStream()
this.conference = await this.getNetwork().joinConference(options)
return this.conference
}
async changeConferenceMedia () {
await this.getNetwork().changeConferenceMedia({
localMediaStream: this.getLocalMediaStream()
})
}
async leaveConference () {
await this.getNetwork().leaveConference()
}
onConferenceParticipantJoined (listener) {
this.events.on('participantJoined', listener)
return this
}
onConferenceParticipantLeft (listener) {
this.events.on('participantLeft', listener)
return this
}
onConferenceEvent (listener) {
this.events.on('conferenceEvent', listener)
return this
}
onConferenceMessage (listener) {
this.events.on('conferenceMessage', listener)
return this
}
onConferenceFile (listener) {
this.events.on('conferenceFile', listener)
return this
}
onConferenceEnded (listener) {
this.events.on('conferenceEnded', listener)
return this
}
onError (listener) {
this.events.on('error', listener)
return this
}
static getInstance () {
if (conferencePlugin === null) {
conferencePlugin = new ConferencePlugin()
}
return conferencePlugin
}
setLocalMediaStream (localMediaStream) {
this.removeLocalMediaStream()
this.localMediaStream = localMediaStream
this.localMediaStream.onEnded(() => {
this.events.emit('localMediaStreamEnded', this.localMediaStream)
})
}
getLocalMediaStream () {
return this.localMediaStream
}
onLocalMediaStreamEnded (listener) {
this.events.on('localMediaStreamEnded', listener)
return this
}
hasLocalMediaStream () {
return this.localMediaStream !== null
}
removeLocalMediaStream () {
if (this.hasLocalMediaStream()) {
this.getLocalMediaStream().stop()
}
this.localMediaStream = null
}
getLocalMediaStreamNative () {
if (this.hasLocalMediaStream()) {
return this.getLocalMediaStream().getStream()
}
return null
}
getLocalParticipant () {
if (this.conference !== null) {
return this.conference.getLocalParticipant()
} else {
return null
}
}
getRemoteParticipant (id) {
if (this.conference !== null) {
return this.conference.getRemoteParticipant(id)
} else {
return null
}
}
}
export default {
install (Vue) {
Vue.$conference = ConferencePlugin.getInstance()
Vue.$conference.setRtcEngine(Vue.$rtcEngine)
}
}

@ -1,189 +0,0 @@
import loadScript from 'load-script'
import EventEmitter from 'events'
const scriptId = 'cdk'
let rtcEnginePlugin = null
export class RtcEnginePlugin {
constructor ({
cdkScriptUrl = null,
webSocketUrl = null,
ngcpApiBaseUrl = null,
ngcpApiJwt = null
}) {
this.cdkScriptUrl = cdkScriptUrl
this.webSocketUrl = webSocketUrl
this.script = null
/**
*
* @type {cdk.Client}
*/
this.client = null
this.sessionToken = null
this.ngcpApiJwt = ngcpApiJwt
this.ngcpApiBaseUrl = ngcpApiBaseUrl
this.events = new EventEmitter()
}
createMedia () {
// eslint-disable-next-line no-undef
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
}
setNgcpApiBaseUrl (baseUrl) {
this.ngcpApiBaseUrl = baseUrl
}
loadLibrary () {
return new Promise((resolve, reject) => {
if (this.script === null) {
loadScript(this.cdkScriptUrl, {
attrs: {
id: scriptId
}
}, (err, script) => {
this.script = script
if (err) {
console.debug(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) {
// eslint-disable-next-line no-undef
cdk.ngcp.setApiBaseUrl(this.ngcpApiBaseUrl)
// eslint-disable-next-line no-undef
cdk.ngcp.setApiJwt(this.ngcpApiJwt)
// eslint-disable-next-line no-undef
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) {
// eslint-disable-next-line no-undef
this.client = new cdk.Client({
url: this.webSocketUrl,
userSession: this.sessionToken
})
this.client.onConnect(() => {
this.events.emit('connected')
try {
const conferenceNetwork = this.client.getNetworkByTag('conference')
conferenceNetwork.onConnect(() => {
this.events.emit('conference-network-connected', conferenceNetwork)
}).onDisconnect(() => {
this.events.emit('conference-network-disconnected', conferenceNetwork)
})
const sipNetwork = this.client.getNetworkByTag('sip')
sipNetwork.onConnect(() => {
this.events.emit('sip-network-connected', sipNetwork)
}).onDisconnect(() => {
this.events.emit('sip-network-disconnected', sipNetwork)
})
} catch (e) {
reject(new Error('Unable to connect to a specific network by RTCEngine client'))
}
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
}
getConferenceNetwork () {
return this.client.getNetworkByTag('conference')
}
static getInstance (rtcConfig = {}) {
if (rtcEnginePlugin === null) {
rtcEnginePlugin = new RtcEnginePlugin(rtcConfig)
}
return rtcEnginePlugin
}
}
export default function getVuePlugin (rtcConfig) {
return {
install (Vue) {
Vue.$rtcEngine = RtcEnginePlugin.getInstance(rtcConfig)
}
}
}

@ -0,0 +1,54 @@
import loadScript from 'load-script'
const RTC_ENGINE_LIBRARY_ID = 'ngcp-rtc-engine-library'
export const LocalMedia = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
videoOnly: 'videoOnly',
audioScreen: 'audioScreen',
screenOnly: 'screenOnly'
}
export function loadRtcEngineLibrary ({ scriptUrl }) {
return new Promise((resolve, reject) => {
const script = document.getElementById(RTC_ENGINE_LIBRARY_ID)
if (!script) {
loadScript(scriptUrl, {
attrs: {
id: RTC_ENGINE_LIBRARY_ID
}
}, (err) => {
if (err) {
reject(new Error('Unable to load RTC:Engine client library'))
} else {
resolve()
}
})
} else {
resolve()
}
})
}
export function unloadRtcEngineLibrary () {
const script = document.getElementById(RTC_ENGINE_LIBRARY_ID)
if (script) {
script.remove()
}
}
export async function rtcEngineCreateMedia (localMedia) {
// eslint-disable-next-line no-undef
const localMediaBuilder = cdk.media.create()
if (localMedia === LocalMedia.audioOnly || localMedia === LocalMedia.audioVideo ||
localMedia === LocalMedia.audioScreen) {
localMediaBuilder.enableMicrophone()
}
if (localMedia === LocalMedia.audioVideo || localMedia === LocalMedia.videoOnly) {
localMediaBuilder.enableCamera()
} else if (localMedia === LocalMedia.audioScreen || localMedia === LocalMedia.screenOnly) {
localMediaBuilder.enableScreen()
}
return await localMediaBuilder.build()
}

@ -1,374 +0,0 @@
import _ from 'lodash'
import Vue from 'vue'
import {
normalizeDestination
} from '../filters/number-format'
import {
startCase
} from '../filters/string'
import {
i18n
} from 'src/boot/i18n'
export var CallState = {
input: 'input',
initiating: 'initiating',
ringing: 'ringing',
incoming: 'incoming',
established: 'established',
ended: 'ended'
}
export var CallStateTitle = {
get input () { return i18n.t('Start new call') },
get initiating () { return i18n.t('Calling') },
get ringing () { return i18n.t('Ringing at') },
get incoming () { return i18n.t('Incoming call from') },
get established () { return i18n.t('In call with') },
get ended () { return i18n.t('Call ended') }
}
export var MediaType = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
audioScreen: 'audioScreen'
}
export const errorVisibilityTimeout = 5000
export const reinitializeTimeout = 5000
function handleUserMediaError (context, err) {
const errName = _.get(err, 'name', null)
const errMessage = _.get(err, 'message', null)
if (errName === 'UserMediaError' && errMessage === 'Permission denied') {
context.commit('endCall', 'userMediaPermissionDenied')
}
if (errMessage === 'plugin not detected') {
context.commit('desktopSharingInstall')
context.commit('hangUpCall')
} else if (errMessage === 'PermissionDenied') {
context.commit('endCall', 'desktopSharingPermissionDenied')
} else {
context.commit('endCall', errName)
}
}
export default {
namespaced: true,
state: {
callEnabled: false,
endedReason: null,
callState: CallState.input,
number: '',
numberInput: '',
localMediaStream: null,
remoteMediaStream: null,
caller: false,
callee: false,
desktopSharingInstall: false,
microphoneEnabled: true,
cameraEnabled: true,
remoteVolumeEnabled: true,
maximized: false,
dialpadOpened: false
},
getters: {
isCallEnabled (state) {
return state.callEnabled
},
endedReason (state) {
return state.endedReason
},
callNumber (state) {
return state.number
},
callNumberInput (state) {
return state.numberInput
},
// 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 rootGetters['user/isRtcEngineInitializing']
},
isPreparing (state) {
return state.callState === CallState.input
},
isInitiating (state) {
return state.callState === CallState.initiating
},
isIncoming (state) {
return state.callState === CallState.incoming
},
isTrying (state) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing
},
isRinging (state) {
return state.callState === CallState.ringing
},
isCalling (state) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.established ||
state.callState === CallState.incoming ||
state.callState === CallState.ended
},
isEstablished (state) {
return state.callState === CallState.established
},
isEnded (state) {
return state.callState === CallState.ended
},
hasRtcEngineCapability (state, getters, rootState, rootGetters) {
return rootGetters['user/hasRtcEngineCapability']
},
hasRtcEngineCapabilityEnabled (state, getters, rootState, rootGetters) {
return rootGetters['user/hasRtcEngineCapabilityEnabled']
},
hasRemoteVideo (state) {
if (state.remoteMediaStream !== null) {
return Vue.$call.hasRemoteVideo()
}
},
hasLocalVideo (state) {
if (state.localMediaStream !== null) {
return Vue.$call.hasLocalVideo()
}
},
hasVideo (state, getters) {
return getters.hasLocalVideo || getters.hasRemoteVideo
},
isAudioEnabled (state) {
return state.audioEnabled
},
isVideoEnabled (state) {
return state.videoEnabled
},
isCaller (state) {
return state.caller
},
isCallee (state) {
return state.callee
},
callState (state) {
return state.callState
},
desktopSharingInstall (state) {
return state.desktopSharingInstall
},
localMediaStream (state) {
if (state.localMediaStream !== null) {
return Vue.$call.getLocalMediaStream()
}
return null
},
remoteMediaStream (state) {
if (state.remoteMediaStream !== null) {
return Vue.$call.getRemoteMediaStream()
}
return null
},
isMicrophoneEnabled (state) {
return state.microphoneEnabled
},
isCameraEnabled (state) {
return state.cameraEnabled
},
isRemoteVolumeEnabled (state) {
return state.remoteVolumeEnabled
},
isMaximized (state) {
return state.maximized
},
isDialpadOpened (state) {
return state.dialpadOpened
},
callNumberFormatted (state, getters) {
return normalizeDestination(getters.callNumber)
},
callEndedReasonFormatted (state, getters) {
return startCase(getters.endedReason)
},
callStateTitle (state) {
return CallStateTitle[state.callState]
},
callStateSubtitle (state, getters) {
if (state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.incoming ||
state.callState === CallState.established) {
return getters.callNumberFormatted
} else if (state.callState === CallState.ended) {
return getters.callEndedReasonFormatted
} else {
return ''
}
}
},
mutations: {
numberInputChanged (state, numberInput) {
state.numberInput = numberInput
},
inputNumber (state) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
},
startCalling (state, number) {
state.number = number
state.callState = CallState.initiating
state.caller = true
state.callee = false
state.endedReason = null
},
localMediaSuccess (state) {
state.localMediaStream = Vue.$call.getLocalMediaId()
},
startRinging (state) {
state.callState = CallState.ringing
},
stopRinging (state) {
state.callState = CallState.established
},
establishCall (state) {
state.remoteMediaStream = Vue.$call.getRemoteMediaId()
state.callState = CallState.established
state.microphoneEnabled = true
state.cameraEnabled = true
state.remoteVolumeEnabled = true
},
incomingCall (state, options) {
state.callState = CallState.incoming
state.number = options.number
state.callee = true
state.caller = false
state.endedReason = null
},
hangUpCall (state) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
Vue.$call.hangUp()
},
endCall (state, reason) {
if (state.endedReason === null) {
state.callState = CallState.ended
state.endedReason = reason
}
Vue.$call.end()
},
sendDTMF (state, value) {
state.dtmf = value
},
toggleMicrophone (state) {
state.microphoneEnabled = !state.microphoneEnabled
},
toggleCamera (state) {
state.cameraEnabled = !state.cameraEnabled
},
toggleRemoteVolume (state) {
state.remoteVolumeEnabled = !state.remoteVolumeEnabled
},
maximize (state) {
state.dialpadOpened = false
state.maximized = true
},
minimize (state) {
state.dialpadOpened = false
state.maximized = false
},
toggleDialpad (state) {
state.dialpadOpened = !state.dialpadOpened
},
enableCall (state) {
state.callEnabled = true
},
disableCall (state) {
state.callEnabled = false
}
},
actions: {
start (context, localMedia) {
const number = context.getters.callNumberInput.replaceAll('(', '')
.replaceAll(')', '')
.replaceAll(' ', '')
.replaceAll('-', '')
context.commit('startCalling', number)
Promise.resolve().then(() => {
return Vue.$call.createLocalMedia(localMedia)
}).then((localMediaStream) => {
context.commit('localMediaSuccess')
Vue.$call.onRingingStart(() => {
context.commit('startRinging')
}).onRingingStop(() => {
context.commit('stopRinging')
}).start(number, localMediaStream)
}).catch((err) => {
Vue.$call.end()
handleUserMediaError(context, err)
setTimeout(() => {
context.commit('inputNumber')
}, errorVisibilityTimeout)
})
},
accept (context, localMedia) {
Vue.$call.createLocalMedia(localMedia).then((localMediaStream) => {
Vue.$call.accept(localMediaStream)
context.commit('localMediaSuccess')
}).catch((err) => {
Vue.$call.end()
handleUserMediaError(context, err)
setTimeout(() => {
context.commit('inputNumber')
}, errorVisibilityTimeout)
})
},
end (context) {
Vue.$call.end()
context.commit('hangUpCall')
},
sendDTMF (context, value) {
if (Vue.$call.hasRunningCall()) {
Vue.$call.sendDTMF(value)
}
},
toggleMicrophone (context) {
if (context.getters.isMicrophoneEnabled) {
Vue.$call.disableAudio()
} else {
Vue.$call.enableAudio()
}
context.commit('toggleMicrophone')
},
toggleCamera (context) {
if (context.getters.isCameraEnabled) {
Vue.$call.disableVideo()
} else {
Vue.$call.enableVideo()
}
context.commit('toggleCamera')
},
toggleRemoteVolume (context) {
context.commit('toggleRemoteVolume')
}
}
}

@ -0,0 +1,73 @@
import {
callGetLocalMediaStreamId,
callAccept,
callEnd,
callStart,
callAddCamera,
callAddScreen,
callRemoveVideo,
callHasLocalVideo,
callToggleMicrophone,
callIsMuted,
callSendDTMF,
callToggleRemoteAudio,
callIsRemoteAudioMuted, callHasLocalScreen, callHasLocalCamera
} from 'src/api/ngcp-call'
export default {
async start (context, localMedia) {
const number = context.getters.callNumberInput.replaceAll('(', '')
.replaceAll(')', '')
.replaceAll(' ', '')
.replaceAll('-', '')
context.commit('startCalling', number)
await callStart({
number,
localMedia
})
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
async accept (context, localMedia) {
await callAccept({
localMedia
})
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
async toggleMicrophone (context) {
callToggleMicrophone()
context.commit('toggleMicrophone', !callIsMuted())
},
async toggleCamera (context) {
if (!callHasLocalVideo() || callHasLocalScreen()) {
await callAddCamera()
context.commit('disableVideo')
context.commit('enableCamera')
} else {
await callRemoveVideo()
context.commit('disableVideo')
}
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
async toggleScreen (context) {
if (!callHasLocalVideo() || callHasLocalCamera()) {
await callAddScreen()
context.commit('disableVideo')
context.commit('enableScreen')
} else {
await callRemoveVideo()
context.commit('disableVideo')
}
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
end (context) {
callEnd()
context.commit('hangUpCall')
},
sendDTMF (context, tone) {
callSendDTMF(tone)
},
toggleRemoteAudio (context) {
callToggleRemoteAudio()
context.commit('toggleRemoteAudio', !callIsRemoteAudioMuted())
}
}

@ -0,0 +1,29 @@
import {
i18n
} from 'boot/i18n'
export const CallState = {
input: 'input',
initiating: 'initiating',
ringing: 'ringing',
incoming: 'incoming',
established: 'established',
ended: 'ended'
}
export const CallStateTitle = {
get input () { return i18n.t('Start new call') },
get initiating () { return i18n.t('Calling') },
get ringing () { return i18n.t('Ringing at') },
get incoming () { return i18n.t('Incoming call from') },
get established () { return i18n.t('In call with') },
get ended () { return i18n.t('Call ended') }
}
export const MediaType = {
audioOnly: 'audioOnly',
audioVideo: 'audioVideo',
audioScreen: 'audioScreen'
}
export const errorVisibilityTimeout = 5000
export const reinitializeTimeout = 5000

@ -0,0 +1,138 @@
import {
callGetLocalMediaStream,
callGetRemoteMediaStream,
callHasRemoteVideo
} from 'src/api/ngcp-call'
import {
normalizeDestination
} from 'src/filters/number-format'
import {
startCase
} from 'src/filters/string'
import {
CallState,
CallStateTitle
} from 'src/store/call/common'
export default {
isCallEnabled (state) {
return state.callEnabled
},
endedReason (state) {
return state.endedReason
},
callNumber (state) {
return state.number
},
callNumberInput (state) {
return state.numberInput
},
isPreparing (state) {
return state.callState === CallState.input
},
isInitiating (state) {
return state.callState === CallState.initiating
},
isIncoming (state) {
return state.callState === CallState.incoming
},
isTrying (state) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing
},
isRinging (state) {
return state.callState === CallState.ringing
},
isCalling (state) {
return state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.established ||
state.callState === CallState.incoming ||
state.callState === CallState.ended
},
isEstablished (state) {
return state.callState === CallState.established
},
isEnded (state) {
return state.callState === CallState.ended
},
hasRtcEngineCapability (state, getters, rootState, rootGetters) {
return rootGetters['user/hasRtcEngineCapability']
},
hasRtcEngineCapabilityEnabled (state, getters, rootState, rootGetters) {
return rootGetters['user/hasRtcEngineCapabilityEnabled']
},
hasRemoteVideo (state) {
if (state.remoteMediaStream !== null) {
return callHasRemoteVideo()
}
},
hasLocalVideo (state, getters) {
if (state.localMediaStream !== null) {
return getters.isScreenEnabled || getters.isCameraEnabled
}
},
hasVideo (state, getters) {
return getters.hasLocalVideo || getters.hasRemoteVideo
},
isCaller (state) {
return state.caller
},
isCallee (state) {
return state.callee
},
callState (state) {
return state.callState
},
localMediaStream (state) {
if (state.localMediaStream) {
return callGetLocalMediaStream()
}
return null
},
remoteMediaStream (state) {
if (state.remoteMediaStream) {
return callGetRemoteMediaStream()
}
return null
},
isMicrophoneEnabled (state) {
return state.microphoneEnabled
},
isCameraEnabled (state) {
return state.cameraEnabled
},
isScreenEnabled (state) {
return state.screenEnabled
},
isRemoteVolumeEnabled (state) {
return state.remoteAudioEnabled
},
isMaximized (state) {
return state.maximized
},
isDialpadOpened (state) {
return state.dialpadOpened
},
callNumberFormatted (state, getters) {
return normalizeDestination(getters.callNumber)
},
callEndedReasonFormatted (state, getters) {
return startCase(getters.endedReason)
},
callStateTitle (state) {
return CallStateTitle[state.callState]
},
callStateSubtitle (state, getters) {
if (state.callState === CallState.initiating ||
state.callState === CallState.ringing ||
state.callState === CallState.incoming ||
state.callState === CallState.established) {
return getters.callNumberFormatted
} else if (state.callState === CallState.ended) {
return getters.callEndedReasonFormatted
} else {
return ''
}
}
}

@ -0,0 +1,12 @@
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
export default {
namespaced: true,
state: state,
getters: getters,
mutations: mutations,
actions: actions
}

@ -0,0 +1,110 @@
import {
CallState
} from 'src/store/call/common'
export default {
numberInputChanged (state, numberInput) {
state.numberInput = numberInput
},
inputNumber (state) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
},
startCalling (state, number) {
state.number = number
state.callState = CallState.initiating
state.caller = true
state.callee = false
state.endedReason = null
},
localMediaSuccess (state, localMediaStreamId) {
state.localMediaStream = localMediaStreamId
},
startRinging (state) {
state.callState = CallState.ringing
},
stopRinging (state) {
state.callState = CallState.established
},
establishCall (state, {
mediaStreamId,
isLocalAudioMuted = false,
hasLocalCamera = false,
hasLocalScreen = false,
hasRemoteVideo = false,
isRemoteAudioMuted = false
}) {
state.remoteMediaStream = mediaStreamId
state.callState = CallState.established
state.microphoneEnabled = !isLocalAudioMuted
state.cameraEnabled = hasLocalCamera
state.screenEnabled = hasLocalScreen
state.remoteAudioEnabled = !isRemoteAudioMuted
state.remoteVideoEnabled = hasRemoteVideo
},
incomingCall (state, options) {
state.callState = CallState.incoming
state.number = options.number
state.callee = true
state.caller = false
state.endedReason = null
},
hangUpCall (state) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
},
endCall (state, reason) {
if (state.endedReason === null) {
state.callState = CallState.ended
state.endedReason = reason
state.localMediaStream = null
state.remoteMediaStream = null
}
},
sendDTMF (state, value) {
state.dtmf = value
},
toggleMicrophone (state, enabled) {
state.microphoneEnabled = enabled
},
setCamera (state, enabled) {
state.cameraEnabled = enabled
},
enableCamera (state) {
state.cameraEnabled = true
},
enableScreen (state) {
state.screenEnabled = true
},
setScreen (state, enabled) {
state.screenEnabled = enabled
},
disableVideo (state) {
state.cameraEnabled = false
state.screenEnabled = false
},
toggleRemoteAudio (state, enabled) {
state.remoteAudioEnabled = enabled
},
maximize (state) {
state.dialpadOpened = false
state.maximized = true
},
minimize (state) {
state.dialpadOpened = false
state.maximized = false
},
toggleDialpad (state) {
state.dialpadOpened = !state.dialpadOpened
},
enableCall (state) {
state.callEnabled = true
},
disableCall (state) {
state.callEnabled = false
}
}

@ -0,0 +1,23 @@
import {
CallState
} from 'src/store/call/common'
export default {
callEnabled: false,
endedReason: null,
callState: CallState.input,
number: '',
numberInput: '',
localMediaStream: null,
remoteMediaStream: null,
caller: false,
callee: false,
microphoneEnabled: true,
cameraEnabled: false,
screenEnabled: false,
remoteAudioEnabled: true,
remoteVideoEnabled: true,
maximized: false,
dialpadOpened: false
}

@ -5,7 +5,7 @@ import { date } from 'quasar'
import CallBlockingModule from './call-blocking'
import CallForwardingModule from './call-forwarding'
import CallModule from './call'
import CallModule from 'src/store/call'
import CallRecordingsModule from './call-recordings'
import CallSettingsModule from './call-settings'
import ConversationsModule from './conversations'

@ -11,9 +11,6 @@ import {
import {
RequestState
} from './common'
// import {
// loadDeviceModel
// } from '../api/pbx-devices'
import { getNumbers } from '../api/user'
import {
i18n

@ -1,6 +1,5 @@
'use strict'
import Vue from 'vue'
import _ from 'lodash'
import {
RequestState
@ -25,6 +24,7 @@ import {
qrPayload
} from 'src/helpers/qr'
import { date } from 'quasar'
import { callInitialize } from 'src/api/ngcp-call'
import { setLocal } from 'src/storage'
export default {
@ -148,12 +148,6 @@ export default {
return null
}
},
isRtcEngineInitialized (state) {
return state.rtcEngineInitState === RequestState.succeeded
},
isRtcEngineInitializing (state) {
return state.rtcEngineInitState === RequestState.requesting
},
getSubscriber (state) {
return state.subscriber
},
@ -309,19 +303,20 @@ export default {
}
},
actions: {
login (context, options) {
async login (context, options) {
context.commit('loginRequesting')
login(options.username, options.password).then((result) => {
try {
const result = await login(options.username, options.password)
setJwt(result.jwt)
setSubscriberId(result.subscriberId)
context.commit('loginSucceeded', {
jwt: getJwt(),
subscriberId: getSubscriberId()
})
context.dispatch('initUser')
}).catch((err) => {
await context.dispatch('initUser')
} catch (err) {
context.commit('loginFailed', err.message)
})
}
},
logout () {
deleteJwt()
@ -339,21 +334,17 @@ export default {
context.dispatch('logout')
}, context.getters.jwtTTL * 1000)
}
if (context.getters.hasRtcEngineCapabilityEnabled && context.getters.isRtcEngineUiVisible) {
context.commit('rtcEngineInitRequesting')
Vue.$rtcEngine.setNgcpApiJwt(getJwt())
try {
await Vue.$rtcEngine.initialize()
context.commit('rtcEngineInitSucceeded')
} catch (err) {
console.debug(err)
context.commit('rtcEngineInitFailed', err.message)
}
}
if (userData.subscriber.profile_id) {
const profile = await getSubscriberProfile(userData.subscriber.profile_id)
context.commit('setProfile', profile)
}
try {
await callInitialize({
subscriber: userData.subscriber
})
} catch (err) {
console.log(err)
}
await context.dispatch('forwardHome')
} catch (err) {
console.debug(err)

@ -5,7 +5,7 @@ FROM docker.mgm.sipwise.com/sipwise-bullseye:latest
# is updated with the current date. It will force refresh of all
# of the base images and things like `apt-get update` won't be using
# old cached versions when the Dockerfile is built.
ENV REFRESHED_AT 2021-07-08
ENV REFRESHED_AT 2021-09-27
ENV DEBIAN_FRONTEND noninteractive
ENV DISPLAY=:0

@ -1427,6 +1427,13 @@
resolved "https://npm-registry.sipwise.com/@types/cordova/-/cordova-0.0.34.tgz#ea7addf74ecec3d7629827a0c39e2c9addc73d04"
integrity sha1-6nrd907Ow9dimCegw54smt3HPQQ=
"@types/debug@^4.1.5":
version "4.1.7"
resolved "https://npm-registry.sipwise.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
dependencies:
"@types/ms" "*"
"@types/electron-packager@14.0.0":
version "14.0.0"
resolved "https://npm-registry.sipwise.com/@types/electron-packager/-/electron-packager-14.0.0.tgz#f6dab1542fe02a3dd235d9bf66c8cb365f123902"
@ -1543,11 +1550,21 @@
resolved "https://npm-registry.sipwise.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
"@types/ms@*":
version "0.7.31"
resolved "https://npm-registry.sipwise.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node@*":
version "16.10.2"
resolved "https://npm-registry.sipwise.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e"
integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==
"@types/node@^14.14.34":
version "14.17.21"
resolved "https://npm-registry.sipwise.com/@types/node/-/node-14.17.21.tgz#6359d8cf73481e312a43886fa50afc70ce5592c6"
integrity sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==
"@types/q@^1.5.1":
version "1.5.5"
resolved "https://npm-registry.sipwise.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
@ -4013,7 +4030,7 @@ debug@^4.0.1:
dependencies:
ms "^2.1.1"
debug@^4.1.0, debug@^4.1.1:
debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
version "4.3.2"
resolved "https://npm-registry.sipwise.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@ -4880,7 +4897,7 @@ eventemitter3@^4.0.0:
resolved "https://npm-registry.sipwise.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.0.0:
events@^3.0.0, events@^3.3.0:
version "3.3.0"
resolved "https://npm-registry.sipwise.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@ -7457,6 +7474,17 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jssip@3.8.2:
version "3.8.2"
resolved "https://npm-registry.sipwise.com/jssip/-/jssip-3.8.2.tgz#5701ee6cbd4a728676574c9e3a914b9e8d2b7a34"
integrity sha512-YmRIMBkzilZZChAgFY2ksesQmMmJHSSATe8e2kQnFtMLdzmE3a1FhhcnzxxKzj+L2Xn6HOaXB6zczRaZMuXvqg==
dependencies:
"@types/debug" "^4.1.5"
"@types/node" "^14.14.34"
debug "^4.3.1"
events "^3.3.0"
sdp-transform "^2.14.1"
jwt-decode@^2.2.0:
version "2.2.0"
resolved "https://npm-registry.sipwise.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
@ -10384,6 +10412,11 @@ schema-utils@^3.0.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
sdp-transform@^2.14.1:
version "2.14.1"
resolved "https://npm-registry.sipwise.com/sdp-transform/-/sdp-transform-2.14.1.tgz#2bb443583d478dee217df4caa284c46b870d5827"
integrity sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==
seek-bzip@^1.0.5:
version "1.0.6"
resolved "https://npm-registry.sipwise.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4"

Loading…
Cancel
Save