diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 40ec4627..b2495f54 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -132,7 +132,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 }, []); @@ -261,7 +261,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}`, @@ -275,7 +275,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 ad6943b9..b5112e5e 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -18,13 +18,13 @@ import { import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { ClientEvent, - type EventTimelineSetHandlerMap, - EventType, - RoomEvent, type RoomMember, RoomStateEvent, SyncState, type Room as MatrixRoom, + type EventTimelineSetHandlerMap, + EventType, + RoomEvent, } from "matrix-js-sdk"; import { BehaviorSubject, @@ -61,8 +61,6 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { type CallMembership, isLivekitFocus, - isLivekitFocusConfig, - type LivekitFocusConfig, type MatrixRTCSession, MatrixRTCSessionEvent, type MatrixRTCSessionEventHandlerMap, @@ -112,11 +110,11 @@ import { observeSpeaker$ } from "./observeSpeaker"; import { shallowEquals } from "../utils/array"; import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname"; import { type MediaDevices } from "./MediaDevices"; -import { constant, type Behavior } from "./Behavior"; - +import { type Behavior } from "./Behavior"; import { enterRTCSession, getLivekitAlias, + leaveRTCSession, makeFocus, } from "../rtcSessionHelpers"; import { E2eeType } from "../e2ee/e2eeType"; @@ -453,16 +451,6 @@ export class CallViewModel extends ViewModel { ), ); - private readonly memberships$ = this.scope.behavior( - fromEvent( - this.matrixRTCSession, - MatrixRTCSessionEvent.MembershipsChanged, - ).pipe( - startWith(null), - map(() => this.matrixRTCSession.memberships), - ), - ); - private readonly membershipsAndFocusMap$ = this.scope.behavior( this.memberships$.pipe( map((memberships) => @@ -524,17 +512,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(), @@ -622,6 +612,17 @@ export class CallViewModel extends ViewModel { // in a split-brained state. private readonly pretendToBeDisconnected$ = this.reconnecting$; + private readonly memberships$ = this.scope.behavior( + fromEvent( + this.matrixRTCSession, + MatrixRTCSessionEvent.MembershipsChanged, + ).pipe( + startWith(null), + pauseWhen(this.pretendToBeDisconnected$), + map(() => this.matrixRTCSession.memberships), + ), + ); + private readonly participants$ = this.scope .behavior< { @@ -671,17 +672,6 @@ export class CallViewModel extends ViewModel { ) .pipe(startWith([]), pauseWhen(this.pretendToBeDisconnected$)); - private readonly memberships$ = this.scope.behavior( - fromEvent( - this.matrixRTCSession, - MatrixRTCSessionEvent.MembershipsChanged, - ).pipe( - startWith(null), - pauseWhen(this.pretendToBeDisconnected$), - map(() => this.matrixRTCSession.memberships), - ), - ); - /** * Displaynames for each member of the call. This will disambiguate * any displaynames that clashes with another member. Only members @@ -1790,7 +1780,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( @@ -1799,6 +1789,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