TT#145001 Call - Implement Stage-3-WebRTC-API

- Audio call (Chrome => Chrome)
- Audio call (Firefox => Firefox)
- Audio call (Chrome => Firefox)
- Audio call (Firefox => Chrome)

- Video call (Chrome => Chrome)
- Video call (Firefox => Firefox)

- Error message if JsSIP connection error occurs
- Fix error visibility timer

Change-Id: I20d55ba4a4f217fde0b5d46a2615314915ba81f0
pull/9/head
Hans-Peter Herzog 4 years ago
parent 238f78cb05
commit 83d622fab1

@ -8,30 +8,68 @@ let $baseWebSocketUrl = null
let $subscriber = null
let $socket = null
let $userAgent = null
let $isVideoScreen = false
let $outgoingRtcSession = null
let $incomingRtcSession = null
let $localMediaStream = null
let $remoteMediaStream = null
let $isVideoScreen = false
let $videoTransceiver = null
let $audioTransceiver = null
const TERMINATION_OPTIONS = {
status_code: 603,
reason_phrase: 'Decline'
}
const MEDIA_VIDEO_DEFAULT_CONFIG = {
width: {
ideal: 4096
},
height: {
ideal: 2160
}
}
export const callEvent = new EventEmitter()
function handleRemoteMediaStream (trackEvent) {
const stream = trackEvent.streams[0]
if (!$remoteMediaStream) {
$remoteMediaStream = stream
function callTrackMuteHandler () {
if ($audioTransceiver) {
$remoteMediaStream = new MediaStream([
$audioTransceiver.receiver.track
])
callEvent.emit('remoteStream', $remoteMediaStream)
} else if ($remoteMediaStream && $remoteMediaStream.id !== stream.id) {
$remoteMediaStream = stream
}
}
function callTrackUnMuteHandler () {
if ($audioTransceiver && $videoTransceiver) {
$remoteMediaStream = new MediaStream([
$audioTransceiver.receiver.track,
$videoTransceiver.receiver.track
])
callEvent.emit('remoteStream', $remoteMediaStream)
}
}
function handleRemoteMediaStream ({ transceiver }) {
if (!$audioTransceiver && transceiver.receiver.track.kind === 'audio') {
$audioTransceiver = transceiver
} else if (!$videoTransceiver && transceiver.receiver.track.kind === 'video') {
$videoTransceiver = transceiver
$videoTransceiver.receiver.track.onmute = callTrackMuteHandler
$videoTransceiver.receiver.track.onunmute = callTrackUnMuteHandler
}
const tracks = []
if ($audioTransceiver) {
tracks.push($audioTransceiver.receiver.track)
}
if ($videoTransceiver) {
tracks.push($videoTransceiver.receiver.track)
}
$remoteMediaStream = new MediaStream(tracks)
callEvent.emit('remoteStream', $remoteMediaStream)
}
function getSubscriberUri () {
return 'sip:' + $subscriber.username + '@' + $subscriber.domain
}
@ -118,10 +156,6 @@ export async function callStart ({ number }) {
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 () {
@ -165,56 +199,58 @@ export function callGetRtcSession () {
}
}
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)
)
export function callGetRtcConnection () {
return callGetRtcSession().connection
}
async function callStopVideo () {
if ($videoTransceiver && $localMediaStream) {
$localMediaStream.removeTrack($videoTransceiver.sender.track)
$videoTransceiver.sender.track.stop()
$videoTransceiver.direction = 'recvonly'
await $videoTransceiver.sender.replaceTrack(null)
await callRenegotiate()
}
}
export async function callSendVideo (stream, audioMuted) {
const videoTrack = stream.getVideoTracks()[0]
if ($videoTransceiver?.sender?.track) {
$localMediaStream.removeTrack($videoTransceiver.sender.track)
$videoTransceiver.sender.track.stop()
}
$localMediaStream.addTrack(videoTrack)
callEvent.emit('localStream', $localMediaStream)
if (!$videoTransceiver) {
$videoTransceiver = callGetRtcConnection().addTransceiver(videoTrack, { direction: 'sendrecv' })
$videoTransceiver.receiver.track.onmute = callTrackMuteHandler
$videoTransceiver.receiver.track.onunmute = callTrackUnMuteHandler
} else {
$videoTransceiver.direction = 'sendrecv'
await $videoTransceiver.sender.replaceTrack(videoTrack)
}
await callRenegotiate()
}
export async function callAddCamera () {
await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({
video: {
width: {
ideal: 4096
},
height: {
ideal: 2160
}
},
audio: true
}))
$isVideoScreen = false
await callSendVideo(await navigator.mediaDevices.getUserMedia({
video: MEDIA_VIDEO_DEFAULT_CONFIG,
audio: false
}))
}
export async function callAddScreen () {
$isVideoScreen = true
await callChangeVideoStream(await navigator.mediaDevices.getDisplayMedia({
video: {
width: {
ideal: 4096
},
height: {
ideal: 2160
}
},
audio: true
await callSendVideo(await navigator.mediaDevices.getDisplayMedia({
video: MEDIA_VIDEO_DEFAULT_CONFIG,
audio: false
}))
}
export async function callRemoveVideo () {
$isVideoScreen = false
await callChangeVideoStream(await navigator.mediaDevices.getUserMedia({
video: false,
audio: true
}))
await callStopVideo()
}
export async function callRenegotiate () {
@ -226,11 +262,11 @@ export async function callRenegotiate () {
}
export function callHasRemoteVideo () {
return $remoteMediaStream?.getVideoTracks?.()?.length > 0
return $videoTransceiver?.receiver?.track?.enabled
}
export function callHasLocalVideo () {
return $localMediaStream?.getVideoTracks?.()?.length > 0
return $videoTransceiver?.sender?.track?.enabled
}
export function callHasLocalCamera () {
@ -242,21 +278,24 @@ export function callHasLocalScreen () {
}
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)
if ($audioTransceiver?.sender?.track) {
$audioTransceiver.sender.track.enabled = !$audioTransceiver.sender.track.enabled
}
}
export function callMute () {
return callGetRtcSession()?.mute()
}
export function callUnMute () {
return callGetRtcSession()?.unmute()
}
export function callIsMuted () {
return callGetRtcSession()?.isMuted()?.audio
if ($audioTransceiver?.sender?.track) {
return !$audioTransceiver.sender.track.enabled
}
return false
}
export function callSendDTMF (tone, transport = 'RFC2833') {
@ -268,31 +307,59 @@ export function callSendDTMF (tone, transport = 'RFC2833') {
}
}
/**
* Enables or disables the remote audio depending on the current state.
*/
export function callToggleRemoteAudio () {
if ($remoteMediaStream && $remoteMediaStream.getAudioTracks()[0]) {
$remoteMediaStream.getAudioTracks()[0].enabled = !$remoteMediaStream?.getAudioTracks()[0]?.enabled
if ($audioTransceiver?.receiver?.track) {
$audioTransceiver.receiver.track.enabled = !$audioTransceiver.receiver.track.enabled
}
}
export function callIsRemoteAudioMuted () {
return !$remoteMediaStream?.getAudioTracks()[0]?.enabled
export function callMuteRemote () {
if ($audioTransceiver?.receiver?.track) {
$audioTransceiver.receiver.track.enabled = false
}
}
export function callEnd () {
if ($outgoingRtcSession && !$outgoingRtcSession.isEnded()) {
$outgoingRtcSession.terminate(TERMINATION_OPTIONS)
$outgoingRtcSession = null
export function callUnMuteRemote () {
if ($audioTransceiver?.receiver?.track) {
$audioTransceiver.receiver.track.enabled = true
}
if ($incomingRtcSession && !$incomingRtcSession.isEnded()) {
$incomingRtcSession.terminate(TERMINATION_OPTIONS)
$incomingRtcSession = null
}
/**
* Checks whether remote audio is muted or not.
* @returns {boolean}
*/
export function callIsRemoteMuted () {
return !$audioTransceiver?.receiver?.track?.enabled
}
/**
* Terminates the call if not ended and cleans up all related resources.
*/
export function callEnd () {
const rtcSession = callGetRtcSession()
if (rtcSession && !rtcSession.isEnded()) {
rtcSession.terminate(TERMINATION_OPTIONS)
}
if ($localMediaStream) {
$localMediaStream.getTracks().forEach(track => track.stop())
try {
if ($localMediaStream) {
$localMediaStream.getTracks().forEach(track => track.stop())
}
} finally {
$localMediaStream = null
}
if ($remoteMediaStream) {
$remoteMediaStream.getTracks().forEach(track => track.stop())
try {
if ($remoteMediaStream) {
$remoteMediaStream.getTracks().forEach(track => track.stop())
}
} finally {
$remoteMediaStream = null
}
$outgoingRtcSession = null
$incomingRtcSession = null
$audioTransceiver = null
$videoTransceiver = null
}

@ -1,19 +1,16 @@
import {
v4
} from 'uuid'
import _ from 'lodash'
import {
callConfigure,
callEnd,
callEvent,
callHasLocalCamera,
callHasLocalScreen,
callHasRemoteVideo,
callIsMuted,
callIsRemoteAudioMuted
callMute,
callMuteRemote,
callUnMute,
callUnMuteRemote
} from 'src/api/ngcp-call'
import { errorVisibilityTimeout } from 'src/store/call/common'
export default async ({ Vue, app, store }) => {
callConfigure({
@ -25,19 +22,21 @@ export default async ({ Vue, app, store }) => {
if (reason?.text) {
cause = reason.text
}
if (event.originator !== 'local') {
callEnd()
store.commit('call/endCall', cause)
setTimeout(() => {
store.commit('call/inputNumber')
}, errorVisibilityTimeout)
}
store.dispatch('call/end', { cause })
}
callEvent.on('connected', () => {
store.commit('call/enableCall')
})
callEvent.on('disconnected', () => {
store.commit('call/disableCall')
callEvent.on('disconnected', ({ error, code }) => {
let errorMessage = null
if (error) {
errorMessage = app.i18n.t('WebSocket connection to kamailio lb failed with code {code}', {
code: code
})
}
store.commit('call/disableCall', {
error: errorMessage
})
})
callEvent.on('outgoingProgress', (event) => {
store.commit('call/startRinging')
@ -51,17 +50,25 @@ export default async ({ Vue, app, store }) => {
number: _.get(session, 'remote_identity.uri.user', 'Unknown')
})
})
callEvent.on('localStream', () => {
store.commit('call/toggleMicrophone', !callIsMuted())
callEvent.on('localStream', (stream) => {
if (store.state.call.microphoneEnabled) {
callUnMute()
} else {
callMute()
}
store.commit('call/localMediaSuccess', stream.id)
})
callEvent.on('remoteStream', () => {
callEvent.on('remoteStream', (stream) => {
if (store.state.call.remoteAudioEnabled) {
callUnMuteRemote()
} else {
callMuteRemote()
}
store.commit('call/establishCall', {
mediaStreamId: v4(),
isLocalAudioMuted: callIsMuted(),
mediaStreamId: stream.id,
hasLocalCamera: callHasLocalCamera(),
hasLocalScreen: callHasLocalScreen(),
hasRemoteVideo: callHasRemoteVideo(),
isRemoteAudioMuted: callIsRemoteAudioMuted()
hasRemoteVideo: callHasRemoteVideo()
})
})
}

@ -2,7 +2,11 @@
<q-banner
:class="bannerClasses"
inline-actions
rounded
v-bind="$attrs"
v-on="$listeners"
>
<slot />
<template
v-if="icon !== null && icon !== undefined"
v-slot:avatar
@ -13,7 +17,6 @@
size="24px"
/>
</template>
<slot />
<template
v-slot:action
>
@ -39,7 +42,7 @@ export default {
},
computed: {
bannerClasses () {
return ['text-dark', 'bg-' + this.color]
return ['text-weight-bold', 'text-dark', 'bg-' + this.color, 'content-start']
}
}
}

@ -1,6 +1,6 @@
<template>
<csc-inline-alert
icon="alert"
icon="error"
color="negative"
v-bind="$attrs"
v-on="$listeners"

@ -471,6 +471,7 @@
"We": "We",
"Web Password": "Web Password",
"Web Password confirm": "Web Password confirm",
"WebSocket connection to kamailio lb failed with code {code}": "WebSocket connection to kamailio lb failed with code {code}",
"Wednesday": "Wednesday",
"Weekly": "Weekly",
"When I dial {slot} ...": "When I dial {slot} ...",

@ -6,12 +6,19 @@
<div
class="col-xs-10 col-sm-8 col-md-4 csc-opt-center"
>
<csc-inline-alert-alert
v-if="connectionError"
class="q-mb-md"
>
{{ connectionError }}
</csc-inline-alert-alert>
<csc-input
id="csc-call-number-input"
:label="$t('Enter a number to dial')"
:value="callNumberInput"
:readonly="dialpadOpened"
clearable
:disable="!isCallEnabled"
@keypress.space.prevent
@keydown.space.prevent
@keyup.space.prevent
@ -47,6 +54,7 @@ import {
import CscCallDialpad from 'components/CscCallDialpad'
import CscPage from 'components/CscPage'
import CscInput from 'components/form/CscInput'
import CscInlineAlertAlert from 'components/CscInlineAlertAlert'
export default {
name: 'CscPageHome',
@ -56,6 +64,7 @@ export default {
}
},
components: {
CscInlineAlertAlert,
CscInput,
CscPage,
CscCallDialpad
@ -75,7 +84,8 @@ export default {
'callNumberInput',
'isCallEnabled',
'callStateTitle',
'callStateSubtitle'
'callStateSubtitle',
'connectionError'
]),
dialpadOpened () {
return this.callState === 'input' && this.isMobile

@ -11,8 +11,13 @@ import {
callIsMuted,
callSendDTMF,
callToggleRemoteAudio,
callIsRemoteAudioMuted, callHasLocalScreen, callHasLocalCamera
callIsRemoteMuted,
callHasLocalScreen,
callHasLocalCamera
} from 'src/api/ngcp-call'
import { errorVisibilityTimeout } from 'src/store/call/common'
let errorVisibilityTimer = null
export default {
async start (context, localMedia) {
@ -37,6 +42,10 @@ export default {
callToggleMicrophone()
context.commit('toggleMicrophone', !callIsMuted())
},
toggleRemoteAudio (context) {
callToggleRemoteAudio()
context.commit('toggleRemoteAudio', !callIsRemoteMuted())
},
async toggleCamera (context) {
if (!callHasLocalVideo() || callHasLocalScreen()) {
await callAddCamera()
@ -46,7 +55,6 @@ export default {
await callRemoveVideo()
context.commit('disableVideo')
}
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
async toggleScreen (context) {
if (!callHasLocalVideo() || callHasLocalCamera()) {
@ -57,17 +65,24 @@ export default {
await callRemoveVideo()
context.commit('disableVideo')
}
context.commit('localMediaSuccess', callGetLocalMediaStreamId())
},
end (context) {
end (context, options = { cause: null }) {
callEnd()
context.commit('hangUpCall')
if (!options.cause) {
if (errorVisibilityTimer) {
clearTimeout(errorVisibilityTimer)
}
context.commit('endCall')
context.commit('hangUpCall')
} else if (options.cause && !errorVisibilityTimer) {
context.commit('endCall', options.cause)
errorVisibilityTimer = setTimeout(() => {
context.commit('hangUpCall')
errorVisibilityTimer = null
}, errorVisibilityTimeout)
}
},
sendDTMF (context, tone) {
callSendDTMF(tone)
},
toggleRemoteAudio (context) {
callToggleRemoteAudio()
context.commit('toggleRemoteAudio', !callIsRemoteAudioMuted())
}
}

@ -134,5 +134,8 @@ export default {
} else {
return ''
}
},
connectionError (state) {
return state.connectionError
}
}

@ -30,18 +30,14 @@ export default {
},
establishCall (state, {
mediaStreamId,
isLocalAudioMuted = false,
hasLocalCamera = false,
hasLocalScreen = false,
hasRemoteVideo = false,
isRemoteAudioMuted = false
hasRemoteVideo = false
}) {
state.remoteMediaStream = mediaStreamId
state.callState = CallState.established
state.microphoneEnabled = !isLocalAudioMuted
state.remoteMediaStream = mediaStreamId
state.cameraEnabled = hasLocalCamera
state.screenEnabled = hasLocalScreen
state.remoteAudioEnabled = !isRemoteAudioMuted
state.remoteVideoEnabled = hasRemoteVideo
},
incomingCall (state, options) {
@ -52,37 +48,36 @@ export default {
state.endedReason = null
},
hangUpCall (state) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
if (state.callState !== CallState.input) {
state.callState = CallState.input
state.number = ''
state.numberInput = ''
state.endedReason = null
}
},
endCall (state, reason) {
if (state.endedReason === null) {
if (reason) {
state.callState = CallState.ended
state.endedReason = reason
state.localMediaStream = null
state.remoteMediaStream = null
}
},
sendDTMF (state, value) {
state.dtmf = value
state.dialpadOpened = false
state.microphoneEnabled = true
state.cameraEnabled = false
state.screenEnabled = false
state.remoteAudioEnabled = true
state.remoteVideoEnabled = false
state.localMediaStream = null
state.remoteMediaStream = null
},
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
@ -103,8 +98,10 @@ export default {
},
enableCall (state) {
state.callEnabled = true
state.connectionError = null
},
disableCall (state) {
disableCall (state, options = { error: null }) {
state.callEnabled = false
state.connectionError = options.error
}
}

@ -4,6 +4,7 @@ import {
} from 'src/store/call/common'
export default {
connectionError: null,
callEnabled: false,
endedReason: null,
callState: CallState.input,

Loading…
Cancel
Save