diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 577b28d3..02fcd64b 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -251,12 +251,15 @@ 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(); + 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); @@ -272,7 +275,7 @@ test.skip("Should close widget when all other left and have time to play a sound 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(), @@ -283,16 +286,17 @@ test.skip("Should close widget when all other left and have time to play a sound const leaveButton = getByText("SimulateOtherLeft"); await user.click(leaveButton); await flushPromises(); - expect(widgetSendMock).not.toHaveBeenCalled(); + expect(widgetClosedCalled).toBeFalsy(); resolvePlaySound.resolve(); - await flushPromises(); - expect(playSound).toHaveBeenCalledWith("left"); - - await widgetClosedCalled.promise; + // 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 widgetClosedPromise; await flushPromises(); + expect(widgetClosedCalled).toBeTruthy(); expect(widgetStopMock).toHaveBeenCalledOnce(); -}); +}, 80000); test("Should close widget when all other left", async () => { const user = userEvent.setup(); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 95b77e73..7c9009fe 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -120,7 +120,7 @@ export const GroupCallView: FC = ({ const muteAllAudio = useBehavior(muteAllAudio$); const leaveSoundContext = useLatest( - useAudioContext({ + useAudioContext({ sounds: callEventAudioSounds, latencyHint: "interactive", muted: muteAllAudio, @@ -318,12 +318,26 @@ 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. 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": + 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) => { 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, ); },