From 83d622fab1d29a6ab12a755b14d21a4e514d5ed5 Mon Sep 17 00:00:00 2001 From: Hans-Peter Herzog Date: Tue, 12 Oct 2021 16:21:52 +0200 Subject: [PATCH] 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 --- src/api/ngcp-call.js | 215 ++++++++++++++++--------- src/boot/ngcp-call.js | 53 +++--- src/components/CscInlineAlert.vue | 7 +- src/components/CscInlineAlertAlert.vue | 2 +- src/i18n/en.json | 1 + src/pages/CscPageHome.vue | 12 +- src/store/call/actions.js | 33 ++-- src/store/call/getters.js | 3 + src/store/call/mutations.js | 43 +++-- src/store/call/state.js | 1 + 10 files changed, 237 insertions(+), 133 deletions(-) diff --git a/src/api/ngcp-call.js b/src/api/ngcp-call.js index 2b562925..08e73517 100644 --- a/src/api/ngcp-call.js +++ b/src/api/ngcp-call.js @@ -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 } diff --git a/src/boot/ngcp-call.js b/src/boot/ngcp-call.js index 51321838..871999e9 100644 --- a/src/boot/ngcp-call.js +++ b/src/boot/ngcp-call.js @@ -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() }) }) } diff --git a/src/components/CscInlineAlert.vue b/src/components/CscInlineAlert.vue index e6416069..2f5db9c1 100644 --- a/src/components/CscInlineAlert.vue +++ b/src/components/CscInlineAlert.vue @@ -2,7 +2,11 @@ + -