Add support for playing a sound when the user exits a call.

This commit is contained in:
Will Hunt
2024-12-02 16:50:59 +00:00
committed by Half-Shot
parent 1f331807d1
commit 2336046539
2 changed files with 70 additions and 26 deletions

View File

@@ -42,6 +42,9 @@ import { useUrlParams } from "../UrlParams";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { Link } from "../button/Link"; import { Link } from "../button/Link";
import leftCallSoundMp3 from "../sound/left_call.mp3";
import leftCallSoundOgg from "../sound/left_call.ogg";
declare global { declare global {
interface Window { interface Window {
rtcSession?: MatrixRTCSession; rtcSession?: MatrixRTCSession;
@@ -72,6 +75,22 @@ export const GroupCallView: FC<Props> = ({
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const isJoined = useMatrixRTCSessionJoinState(rtcSession); const isJoined = useMatrixRTCSessionJoinState(rtcSession);
// Only used in widget mode.
const leaveSound = useRef<HTMLAudioElement|null>(null);
const playLeaveSound = useCallback(async () => {
if (!leaveSound.current) {
return;
}
const ended = new Promise<void>(r => {
leaveSound.current?.addEventListener("ended", () => r());
});
console.log('Playing');
await leaveSound.current.play();
console.log('started playing');
await ended;
console.log('ended');
}, [leaveSound]);
// This should use `useEffectEvent` (only available in experimental versions) // This should use `useEffectEvent` (only available in experimental versions)
useEffect(() => { useEffect(() => {
if (memberships.length >= MUTE_PARTICIPANT_COUNT) if (memberships.length >= MUTE_PARTICIPANT_COUNT)
@@ -214,12 +233,11 @@ export const GroupCallView: FC<Props> = ({
const onLeave = useCallback( const onLeave = useCallback(
(leaveError?: Error): void => { (leaveError?: Error): void => {
setLeaveError(leaveError);
setLeft(true);
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent, // In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
// therefore we want the event to be sent instantly without getting queued/batched. // therefore we want the event to be sent instantly without getting queued/batched.
const sendInstantly = !!widget; const sendInstantly = !!widget;
console.log('hangup!', sendInstantly);
setLeaveError(leaveError);
PosthogAnalytics.instance.eventCallEnded.track( PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId, rtcSession.room.roomId,
rtcSession.memberships.length, rtcSession.memberships.length,
@@ -227,20 +245,22 @@ export const GroupCallView: FC<Props> = ({
rtcSession, rtcSession,
); );
// Wait for the sound in widget mode (it's not long)
leaveRTCSession(rtcSession, sendInstantly ? playLeaveSound() : undefined)
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts. // Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
leaveRTCSession(rtcSession) .then(() => {
.then(() => { setLeft(true);
if ( if (
!isPasswordlessUser && !isPasswordlessUser &&
!confineToRoom && !confineToRoom &&
!PosthogAnalytics.instance.isEnabled() !PosthogAnalytics.instance.isEnabled()
) { ) {
history.push("/"); history.push("/");
} }
}) })
.catch((e) => { .catch((e) => {
logger.error("Error leaving RTC session", e); logger.error("Error leaving RTC session", e);
}); });
}, },
[rtcSession, isPasswordlessUser, confineToRoom, history], [rtcSession, isPasswordlessUser, confineToRoom, history],
); );
@@ -308,6 +328,14 @@ export const GroupCallView: FC<Props> = ({
onDismiss={onDismissInviteModal} onDismiss={onDismissInviteModal}
/> />
); );
const callEndedAudio = <audio
ref={leaveSound}
preload="auto"
hidden
>
<source src={leftCallSoundOgg} type="audio/ogg; codecs=vorbis" />
<source src={leftCallSoundMp3} type="audio/mpeg" />
</audio>;
const lobbyView = ( const lobbyView = (
<> <>
{shareModal} {shareModal}
@@ -321,6 +349,7 @@ export const GroupCallView: FC<Props> = ({
participantCount={participantCount} participantCount={participantCount}
onShareClick={onShareClick} onShareClick={onShareClick}
/> />
{callEndedAudio}
</> </>
); );
@@ -340,6 +369,7 @@ export const GroupCallView: FC<Props> = ({
//otelGroupCallMembership={otelGroupCallMembership} //otelGroupCallMembership={otelGroupCallMembership}
onShareClick={onShareClick} onShareClick={onShareClick}
/> />
{callEndedAudio}
</> </>
); );
} else if (left && widget === null) { } else if (left && widget === null) {
@@ -357,14 +387,22 @@ export const GroupCallView: FC<Props> = ({
leaveError leaveError
) { ) {
return ( return (
<CallEndedView <>
endedCallId={rtcSession.room.roomId} <CallEndedView
client={client} endedCallId={rtcSession.room.roomId}
isPasswordlessUser={isPasswordlessUser} client={client}
confineToRoom={confineToRoom} isPasswordlessUser={isPasswordlessUser}
leaveError={leaveError} confineToRoom={confineToRoom}
reconnect={onReconnect} leaveError={leaveError}
/> reconnect={onReconnect}
/><audio
autoPlay
hidden
>
<source src={leftCallSoundOgg} type="audio/ogg; codecs=vorbis" />
<source src={leftCallSoundMp3} type="audio/mpeg" />
</audio>;
</>
); );
} else { } else {
// If the user is a regular user, we'll have sent them back to the homepage, // If the user is a regular user, we'll have sent them back to the homepage,
@@ -375,7 +413,7 @@ export const GroupCallView: FC<Props> = ({
} else if (left && widget !== null) { } else if (left && widget !== null) {
// Left in widget mode: // Left in widget mode:
if (!returnToLobby) { if (!returnToLobby) {
return null; return callEndedAudio;
} }
} else if (preload || skipLobby) { } else if (preload || skipLobby) {
return null; return null;

View File

@@ -120,6 +120,7 @@ export async function enterRTCSession(
const widgetPostHangupProcedure = async ( const widgetPostHangupProcedure = async (
widget: WidgetHelpers, widget: WidgetHelpers,
promiseBeforeHangup?: Promise<unknown>,
): Promise<void> => { ): Promise<void> => {
// we need to wait until the callEnded event is tracked on posthog. // we need to wait until the callEnded event is tracked on posthog.
// Otherwise the iFrame gets killed before the callEnded event got tracked. // Otherwise the iFrame gets killed before the callEnded event got tracked.
@@ -132,6 +133,8 @@ const widgetPostHangupProcedure = async (
logger.error("Failed to set call widget `alwaysOnScreen` to false", e); logger.error("Failed to set call widget `alwaysOnScreen` to false", e);
} }
// Wait for any last bits before hanging up.
await promiseBeforeHangup;
// We send the hangup event after the memberships have been updated // We send the hangup event after the memberships have been updated
// calling leaveRTCSession. // calling leaveRTCSession.
// We need to wait because this makes the client hosting this widget killing the IFrame. // We need to wait because this makes the client hosting this widget killing the IFrame.
@@ -140,9 +143,12 @@ const widgetPostHangupProcedure = async (
export async function leaveRTCSession( export async function leaveRTCSession(
rtcSession: MatrixRTCSession, rtcSession: MatrixRTCSession,
promiseBeforeHangup?: Promise<unknown>,
): Promise<void> { ): Promise<void> {
await rtcSession.leaveRoomSession(); await rtcSession.leaveRoomSession();
if (widget) { if (widget) {
await widgetPostHangupProcedure(widget); await widgetPostHangupProcedure(widget, promiseBeforeHangup);
} else {
await promiseBeforeHangup;
} }
} }