From ea4144ccc79edd96806d635a479adcf788f83fd3 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 28 May 2026 12:04:42 +0200 Subject: [PATCH 1/5] Fix play of second leave sound --- src/room/GroupCallView.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 95b77e73..009ac3a6 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -318,12 +318,24 @@ export const GroupCallView: FC = ({ ( reason: "timeout" | "user" | "allOthersLeft" | "decline" | "error", ): void => { - let playSound: CallEventSounds = "left"; - if (reason === "timeout" || reason === "decline") playSound = reason; + let audioPromise: Promise | undefined = undefined; + switch (reason) { + case "allOthersLeft": + // When "allOthersLeft", the leaveSoundEffect$ in CallEventAudioRenderer + // already plays the "left" sound when the remote participant's media + // disappears. Playing it here too would cause the sound to play twice. + break; + case "timeout": + case "decline": + audioPromise = leaveSoundContext.current?.playSound(reason); + break; + default: + audioPromise = leaveSoundContext.current?.playSound("left"); + } setJoined(false); setLeft(true); - const audioPromise = leaveSoundContext.current?.playSound(playSound); + // We need to wait until the callEnded event is tracked on PostHog, // otherwise the iframe may get killed first. const posthogRequest = new Promise((resolve) => { From e05af09c28308616f0849a5d5e2fc53c2bc363cb Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 2 Jun 2026 11:27:59 +0200 Subject: [PATCH 2/5] Add test --- src/room/GroupCallView.test.tsx | 7 ++++--- src/room/GroupCallView.tsx | 9 ++++----- src/useAudioContext.tsx | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 2aef571a..97da083e 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -248,7 +248,7 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn expect(leaveRTCSession).toHaveBeenCalledOnce(); }); -test.skip("Should close widget when all other left and have time to play a sound", async () => { +test("Should close widget when all other left and have time to play a sound", async () => { const user = userEvent.setup(); const widgetClosedCalled = Promise.withResolvers(); const widgetSendMock = vi.fn().mockImplementation((action: string) => { @@ -284,8 +284,9 @@ test.skip("Should close widget when all other left and have time to play a sound resolvePlaySound.resolve(); await flushPromises(); - expect(playSound).toHaveBeenCalledWith("left"); - + // Expect the leave sound to be played but silent (volumeOverwrite = 0) + // The allOthersLeft effect should already play a leave sound for the last user in the call. + expect(playSound).toHaveBeenCalledWith("left", 0); await widgetClosedCalled.promise; await flushPromises(); expect(widgetStopMock).toHaveBeenCalledOnce(); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 009ac3a6..73065d36 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -57,10 +57,7 @@ import { } from "../UrlParams"; import { E2eeType } from "../e2ee/e2eeType"; import { useAudioContext } from "../useAudioContext"; -import { - callEventAudioSounds, - type CallEventSounds, -} from "./CallEventAudioRenderer"; +import { callEventAudioSounds } from "./CallEventAudioRenderer"; import { useLatest } from "../useLatest"; import { usePageTitle } from "../usePageTitle"; import { @@ -323,7 +320,9 @@ export const GroupCallView: FC = ({ case "allOthersLeft": // When "allOthersLeft", the leaveSoundEffect$ in CallEventAudioRenderer // already plays the "left" sound when the remote participant's media - // disappears. Playing it here too would cause the sound to play twice. + // disappears. We play it here silenced (volumeOverwrite = 0) so we have the right duration in the audioPromise. + // (used to destory the widget) + audioPromise = leaveSoundContext.current?.playSound("left", 0); break; case "timeout": case "decline": diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index 4d08dde8..4a7c031c 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -114,7 +114,7 @@ interface Props { } interface UseAudioContext { - playSound(soundName: S): Promise; + playSound(soundName: S, volumeOverwrite?: number): Promise; playSoundLooping(soundName: S, delayS?: number): () => Promise; /** * Map of sound name to duration in seconds. @@ -195,7 +195,7 @@ export function useAudioContext( } return { - playSound: async (name): Promise => { + playSound: async (name, volumeOverwrite?: number): Promise => { if (!audioBuffers[name]) { logger.debug(`Tried to play a sound that wasn't buffered (${name})`); return; @@ -203,7 +203,7 @@ export function useAudioContext( return playSound( audioContext, audioBuffers[name], - soundEffectVolume * earpieceVolume, + volumeOverwrite ?? soundEffectVolume * earpieceVolume, earpiecePan, ); }, From 9117b40e7b883c8eb65b9d999a8124eeef98cfc4 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 2 Jun 2026 13:52:43 +0200 Subject: [PATCH 3/5] fix tests --- src/room/GroupCallView.test.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 97da083e..1576fbdc 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -250,10 +250,13 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn test("Should close widget when all other left and have time to play a sound", async () => { const user = userEvent.setup(); - const widgetClosedCalled = Promise.withResolvers(); + let widgetClosedCalled = false; + const { promise: widgetClosedPromise, resolve: widgetClosedResolver } = + Promise.withResolvers(); const widgetSendMock = vi.fn().mockImplementation((action: string) => { if (action === ElementWidgetActions.Close) { - widgetClosedCalled.resolve(); + widgetClosedCalled = true; + widgetClosedResolver(); } }); const widgetStopMock = vi.fn().mockResolvedValue(undefined); @@ -280,17 +283,18 @@ test("Should close widget when all other left and have time to play a sound", as const leaveButton = getByText("SimulateOtherLeft"); await user.click(leaveButton); await flushPromises(); - expect(widgetSendMock).not.toHaveBeenCalled(); + expect(widgetClosedCalled).toBeFalsy(); resolvePlaySound.resolve(); await flushPromises(); // Expect the leave sound to be played but silent (volumeOverwrite = 0) // The allOthersLeft effect should already play a leave sound for the last user in the call. expect(playSound).toHaveBeenCalledWith("left", 0); - await widgetClosedCalled.promise; + await widgetClosedPromise; await flushPromises(); + expect(widgetClosedCalled).toBeTruthy(); expect(widgetStopMock).toHaveBeenCalledOnce(); -}); +}, 80000); test("Should close widget when all other left", async () => { const user = userEvent.setup(); From c5ffdea3707c67e73edc41f7a3b89cd5d62bf53b Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 2 Jun 2026 13:56:16 +0200 Subject: [PATCH 4/5] lint --- src/room/GroupCallView.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 73065d36..7c9009fe 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -57,7 +57,10 @@ import { } from "../UrlParams"; import { E2eeType } from "../e2ee/e2eeType"; import { useAudioContext } from "../useAudioContext"; -import { callEventAudioSounds } from "./CallEventAudioRenderer"; +import { + callEventAudioSounds, + type CallEventSounds, +} from "./CallEventAudioRenderer"; import { useLatest } from "../useLatest"; import { usePageTitle } from "../usePageTitle"; import { @@ -117,7 +120,7 @@ export const GroupCallView: FC = ({ const muteAllAudio = useBehavior(muteAllAudio$); const leaveSoundContext = useLatest( - useAudioContext({ + useAudioContext({ sounds: callEventAudioSounds, latencyHint: "interactive", muted: muteAllAudio, From 692a55c84cce0ae1de0a5597f81cdeffeca43e91 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 2 Jun 2026 14:06:28 +0200 Subject: [PATCH 5/5] fix race --- src/room/GroupCallView.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 1576fbdc..337c489a 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -272,7 +272,7 @@ test("Should close widget when all other left and have time to play a sound", as lazyActions: new LazyEventEmitter(), }; const resolvePlaySound = Promise.withResolvers(); - playSound = vi.fn().mockReturnValue(resolvePlaySound); + playSound = vi.fn().mockReturnValue(resolvePlaySound.promise); (useAudioContext as MockedFunction).mockReturnValue({ playSound, playSoundLooping: vitest.fn(), @@ -285,7 +285,6 @@ test("Should close widget when all other left and have time to play a sound", as await flushPromises(); expect(widgetClosedCalled).toBeFalsy(); resolvePlaySound.resolve(); - await flushPromises(); // Expect the leave sound to be played but silent (volumeOverwrite = 0) // The allOthersLeft effect should already play a leave sound for the last user in the call.