diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index b81a2ef9..7a971c9e 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -129,7 +129,7 @@ export const GroupCallView: FC = ({ // This should use `useEffectEvent` (only available in experimental versions) useEffect(() => { if (memberships.length >= MUTE_PARTICIPANT_COUNT) - muteStates.audio.setEnabled?.(false); + muteStates.audio.setEnabled$.value?.(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -258,7 +258,7 @@ export const GroupCallView: FC = ({ if (!deviceId) { logger.warn("Unknown audio input: " + audioInput); // override the default mute state - latestMuteStates.current!.audio.setEnabled?.(false); + latestMuteStates.current!.audio.setEnabled$.value?.(false); } else { logger.debug( `Found audio input ID ${deviceId} for name ${audioInput}`, @@ -272,7 +272,7 @@ export const GroupCallView: FC = ({ if (!deviceId) { logger.warn("Unknown video input: " + videoInput); // override the default mute state - latestMuteStates.current!.video.setEnabled?.(false); + latestMuteStates.current!.video.setEnabled$.value?.(false); } else { logger.debug( `Found video input ID ${deviceId} for name ${videoInput}`, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 7e16689f..ce2893b2 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -5,14 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - observeParticipantEvents, - observeParticipantMedia, -} from "@livekit/components-core"; +import { observeParticipantEvents } from "@livekit/components-core"; import { type E2EEOptions, ExternalE2EEKeyProvider, - Room as LivekitRoom, + type Room as LivekitRoom, type LocalParticipant, ParticipantEvent, type RemoteParticipant, @@ -20,7 +17,7 @@ import { import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { ClientEvent, - RoomMember, + type RoomMember, RoomStateEvent, SyncState, type Room as MatrixRoom, @@ -53,8 +50,6 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { type CallMembership, isLivekitFocus, - isLivekitFocusConfig, - type LivekitFocusConfig, type MatrixRTCSession, MatrixRTCSessionEvent, MembershipManagerEvent, @@ -104,10 +99,10 @@ import { shallowEquals } from "../utils/array"; import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname"; import { type MediaDevices } from "./MediaDevices"; import { type Behavior } from "./Behavior"; - import { enterRTCSession, getLivekitAlias, + leaveRTCSession, makeFocus, } from "../rtcSessionHelpers"; import { E2eeType } from "../e2ee/e2eeType"; @@ -504,17 +499,19 @@ export class CallViewModel extends ViewModel { ), ); - private readonly joined$ = new Subject(); + private readonly join$ = new Subject(); public join(): void { - this.joined$.next(); + this.join$.next(); } + private readonly leave$ = new Subject(); + public leave(): void { - // TODO + this.leave$.next(); } - private readonly connectionInstructions$ = this.joined$.pipe( + private readonly connectionInstructions$ = this.join$.pipe( switchMap(() => this.remoteConnections$), startWith(new Map()), pairwise(), @@ -1682,7 +1679,7 @@ export class CallViewModel extends ViewModel { for (const connection of start) void connection.start(); for (const connection of stop) connection.stop(); }); - combineLatest([this.localFocus, this.joined$]) + combineLatest([this.localFocus, this.join$]) .pipe(this.scope.bind()) .subscribe(([localFocus]) => { void enterRTCSession( @@ -1691,6 +1688,17 @@ export class CallViewModel extends ViewModel { this.options.encryptionSystem.kind !== E2eeType.PER_PARTICIPANT, ); }); + this.join$.pipe(this.scope.bind()).subscribe(() => { + leaveRTCSession( + this.matrixRTCSession, + "user", // TODO-MULTI-SFU ? + // Wait for the sound in widget mode (it's not long) + Promise.resolve(), // TODO-MULTI-SFU + //Promise.all([audioPromise, posthogRequest]), + ).catch((e) => { + logger.error("Error leaving RTC session", e); + }); + }); // Pause upstream of all local media tracks when we're disconnected from // MatrixRTC, because it can be an unpleasant surprise for the app to say diff --git a/src/state/MuteStates.ts b/src/state/MuteStates.ts index d5425163..4ac095b0 100644 --- a/src/state/MuteStates.ts +++ b/src/state/MuteStates.ts @@ -26,6 +26,7 @@ import { Config } from "../config/Config"; import { getUrlParams } from "../UrlParams"; import { type ObservableScope } from "./ObservableScope"; import { accumulate } from "../utils/observable"; +import { type Behavior } from "./Behavior"; interface MuteStateData { enabled$: Observable; @@ -33,13 +34,13 @@ interface MuteStateData { toggle: (() => void) | null; } -class MuteState { +class MuteState { private readonly enabledByDefault$ = this.enabledByConfig && !getUrlParams().skipLobby - ? this.isJoined$.pipe(map((isJoined) => !isJoined)) + ? this.joined$.pipe(map((isJoined) => !isJoined)) : of(false); - private readonly data$: Observable = + private readonly data$ = this.scope.behavior( this.device.available$.pipe( map((available) => available.size > 0), distinctUntilChanged(), @@ -52,8 +53,8 @@ class MuteState { const set$ = new Subject(); const toggle$ = new Subject(); return { - set: (enabled: boolean) => set$.next(enabled), - toggle: () => toggle$.next(), + set: (enabled: boolean): void => set$.next(enabled), + toggle: (): void => toggle$.next(), // Assume the default value only once devices are actually connected enabled$: merge( set$, @@ -66,24 +67,24 @@ class MuteState { }; }, ), - this.scope.state(), - ); - - public readonly enabled$: Observable = this.data$.pipe( - switchMap(({ enabled$ }) => enabled$), + ), ); - public readonly setEnabled$: Observable<((enabled: boolean) => void) | null> = - this.data$.pipe(map(({ set }) => set)); + public readonly enabled$: Behavior = this.scope.behavior( + this.data$.pipe(switchMap(({ enabled$ }) => enabled$)), + ); - public readonly toggle$: Observable<(() => void) | null> = this.data$.pipe( - map(({ toggle }) => toggle), + public readonly setEnabled$: Behavior<((enabled: boolean) => void) | null> = + this.scope.behavior(this.data$.pipe(map(({ set }) => set))); + + public readonly toggle$: Behavior<(() => void) | null> = this.scope.behavior( + this.data$.pipe(map(({ toggle }) => toggle)), ); public constructor( private readonly scope: ObservableScope, - private readonly device: MediaDevice, - private readonly isJoined$: Observable, + private readonly device: MediaDevice, + private readonly joined$: Observable, private readonly enabledByConfig: boolean, ) {} } @@ -92,20 +93,20 @@ export class MuteStates { public readonly audio = new MuteState( this.scope, this.mediaDevices.audioInput, - this.isJoined$, + this.joined$, Config.get().media_devices.enable_video, ); public readonly video = new MuteState( this.scope, this.mediaDevices.videoInput, - this.isJoined$, + this.joined$, Config.get().media_devices.enable_video, ); public constructor( private readonly scope: ObservableScope, private readonly mediaDevices: MediaDevices, - private readonly isJoined$: Observable, + private readonly joined$: Observable, ) { if (widget !== null) { // Sync our mute states with the hosting client