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: Ife56ca49cadade44ee9b70b77b3f345b262be9d9pull/9/head
parent
3d1c2a7a6a
commit
238f78cb05
@ -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')
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in new issue