From 21f413d6c87cbf402325664da7bbbc9b3d7bce83 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 13 Mar 2025 18:52:10 -0300 Subject: [PATCH] fix: VoIP calls one-way audio issues (#35420) --- .changeset/real-mayflies-provide.md | 5 ++ packages/ui-voip/src/lib/VoipClient.ts | 53 ++++++++++--------- .../ui-voip/src/providers/VoipProvider.tsx | 19 ++++--- 3 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 .changeset/real-mayflies-provide.md diff --git a/.changeset/real-mayflies-provide.md b/.changeset/real-mayflies-provide.md new file mode 100644 index 00000000000..c28f8108f2c --- /dev/null +++ b/.changeset/real-mayflies-provide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/ui-voip': patch +--- + +fixes an issue where audio from VoIP calls would sometimes not be played diff --git a/packages/ui-voip/src/lib/VoipClient.ts b/packages/ui-voip/src/lib/VoipClient.ts index b96adaece96..7f8aa0e5e53 100644 --- a/packages/ui-voip/src/lib/VoipClient.ts +++ b/packages/ui-voip/src/lib/VoipClient.ts @@ -1,5 +1,4 @@ -import type { IMediaStreamRenderer, SignalingSocketEvents, VoipEvents as CoreVoipEvents } from '@rocket.chat/core-typings'; -import { type VoIPUserConfiguration } from '@rocket.chat/core-typings'; +import type { SignalingSocketEvents, VoipEvents as CoreVoipEvents, VoIPUserConfiguration } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { InvitationAcceptOptions, Message, Referral, Session, SessionInviteOptions } from 'sip.js'; import { Registerer, RequestPendingError, SessionState, UserAgent, Invitation, Inviter, RegistererState, UserAgentState } from 'sip.js'; @@ -33,7 +32,7 @@ class VoipClient extends Emitter { public networkEmitter: Emitter; - private mediaStreamRendered: IMediaStreamRenderer | undefined; + private audioElement: HTMLAudioElement | null = null; private remoteStream: RemoteStream | undefined; @@ -47,11 +46,9 @@ class VoipClient extends Emitter { private contactInfo: ContactInfo | null = null; - constructor(private readonly config: VoIPUserConfiguration, mediaRenderer?: IMediaStreamRenderer) { + constructor(private readonly config: VoIPUserConfiguration) { super(); - this.mediaStreamRendered = mediaRenderer; - this.networkEmitter = new Emitter(); } @@ -101,8 +98,8 @@ class VoipClient extends Emitter { } } - static async create(config: VoIPUserConfiguration, mediaRenderer?: IMediaStreamRenderer): Promise { - const voip = new VoipClient(config, mediaRenderer); + static async create(config: VoIPUserConfiguration): Promise { + const voip = new VoipClient(config); await voip.init(); return voip; } @@ -154,7 +151,7 @@ class VoipClient extends Emitter { }); }; - public call = async (calleeURI: string, mediaRenderer?: IMediaStreamRenderer): Promise => { + public call = async (calleeURI: string): Promise => { if (!calleeURI) { throw new Error('Invalid URI'); } @@ -167,10 +164,6 @@ class VoipClient extends Emitter { throw new Error('No User Agent.'); } - if (mediaRenderer) { - this.switchMediaRenderer(mediaRenderer); - } - const target = this.makeURI(calleeURI); if (!target) { @@ -441,14 +434,12 @@ class VoipClient extends Emitter { return true; } - public switchMediaRenderer(mediaRenderer: IMediaStreamRenderer): void { - if (!this.remoteStream) { - return; - } + public switchAudioElement(audioElement: HTMLAudioElement | null): void { + this.audioElement = audioElement; - this.mediaStreamRendered = mediaRenderer; - this.remoteStream.init(mediaRenderer.remoteMediaElement); - this.remoteStream.play(); + if (this.remoteStream) { + this.playRemoteStream(); + } } private setContactInfo(contact: ContactInfo) { @@ -613,6 +604,10 @@ class VoipClient extends Emitter { }; } + public getAudioElement(): HTMLAudioElement | null { + return this.audioElement; + } + public notifyDialer(value: { open: boolean }) { this.emit('dialer', value); } @@ -633,12 +628,22 @@ class VoipClient extends Emitter { const { remoteMediaStream } = this.sessionDescriptionHandler; this.remoteStream = new RemoteStream(remoteMediaStream); - const mediaElement = this.mediaStreamRendered?.remoteMediaElement; + this.playRemoteStream(); + } - if (mediaElement) { - this.remoteStream.init(mediaElement); - this.remoteStream.play(); + private playRemoteStream() { + if (!this.remoteStream) { + console.warn(`Attempted to play missing remote media.`); + return; } + + if (!this.audioElement) { + console.error('Unable to play remote media: VoIPClient is missing an AudioElement reference to play it on.'); + return; + } + + this.remoteStream.init(this.audioElement); + this.remoteStream.play(); } private makeURI(calleeURI: string): URI | undefined { diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index 6fcd3f0341d..d6d302fffd0 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -8,7 +8,7 @@ import { useToastMessageDispatch, } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; -import { useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -39,7 +39,12 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { const dispatchToastMessage = useToastMessageDispatch(); // Refs - const remoteAudioMediaRef = useRef(null); + const remoteAudioMediaRef = useCallback( + (node: HTMLMediaElement | null) => { + voipClient?.switchAudioElement(node); + }, + [voipClient], + ); useEffect(() => { if (!voipClient) { @@ -54,10 +59,6 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { const onCallEstablished = async (): Promise => { voipSounds.stopAll(); window.addEventListener('beforeunload', onBeforeUnload); - - if (voipClient.isCallee() && remoteAudioMediaRef.current) { - voipClient.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current }); - } }; const onNetworkDisconnected = (): void => { @@ -120,11 +121,13 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }, [dispatchToastMessage, setStorageRegistered, t, voipClient, voipSounds]); const changeAudioOutputDevice = useEffectEvent(async (selectedAudioDevice: Device): Promise => { - if (!remoteAudioMediaRef.current) { + const element = voipClient?.getAudioElement(); + if (!element) { + console.warn(`Failed to change audio output device: missing audio element reference.`); return; } - setOutputMediaDevice({ outputDevice: selectedAudioDevice, HTMLAudioElement: remoteAudioMediaRef.current }); + setOutputMediaDevice({ outputDevice: selectedAudioDevice, HTMLAudioElement: element }); }); const changeAudioInputDevice = useEffectEvent(async (selectedAudioDevice: Device): Promise => {