@@ -132,7 +132,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
// 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)
|
||||||
muteStates.audio.setEnabled?.(false);
|
muteStates.audio.setEnabled$.value?.(false);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
logger.warn("Unknown audio input: " + audioInput);
|
logger.warn("Unknown audio input: " + audioInput);
|
||||||
// override the default mute state
|
// override the default mute state
|
||||||
latestMuteStates.current!.audio.setEnabled?.(false);
|
latestMuteStates.current!.audio.setEnabled$.value?.(false);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Found audio input ID ${deviceId} for name ${audioInput}`,
|
`Found audio input ID ${deviceId} for name ${audioInput}`,
|
||||||
@@ -275,7 +275,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
logger.warn("Unknown video input: " + videoInput);
|
logger.warn("Unknown video input: " + videoInput);
|
||||||
// override the default mute state
|
// override the default mute state
|
||||||
latestMuteStates.current!.video.setEnabled?.(false);
|
latestMuteStates.current!.video.setEnabled$.value?.(false);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Found video input ID ${deviceId} for name ${videoInput}`,
|
`Found video input ID ${deviceId} for name ${videoInput}`,
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import {
|
|||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker";
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
type EventTimelineSetHandlerMap,
|
|
||||||
EventType,
|
|
||||||
RoomEvent,
|
|
||||||
type RoomMember,
|
type RoomMember,
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
SyncState,
|
SyncState,
|
||||||
type Room as MatrixRoom,
|
type Room as MatrixRoom,
|
||||||
|
type EventTimelineSetHandlerMap,
|
||||||
|
EventType,
|
||||||
|
RoomEvent,
|
||||||
} from "matrix-js-sdk";
|
} from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -61,8 +61,6 @@ import { logger } from "matrix-js-sdk/lib/logger";
|
|||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
isLivekitFocus,
|
isLivekitFocus,
|
||||||
isLivekitFocusConfig,
|
|
||||||
type LivekitFocusConfig,
|
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
type MatrixRTCSessionEventHandlerMap,
|
type MatrixRTCSessionEventHandlerMap,
|
||||||
@@ -112,11 +110,11 @@ import { observeSpeaker$ } from "./observeSpeaker";
|
|||||||
import { shallowEquals } from "../utils/array";
|
import { shallowEquals } from "../utils/array";
|
||||||
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
|
||||||
import { type MediaDevices } from "./MediaDevices";
|
import { type MediaDevices } from "./MediaDevices";
|
||||||
import { constant, type Behavior } from "./Behavior";
|
import { type Behavior } from "./Behavior";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
enterRTCSession,
|
enterRTCSession,
|
||||||
getLivekitAlias,
|
getLivekitAlias,
|
||||||
|
leaveRTCSession,
|
||||||
makeFocus,
|
makeFocus,
|
||||||
} from "../rtcSessionHelpers";
|
} from "../rtcSessionHelpers";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
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(
|
private readonly membershipsAndFocusMap$ = this.scope.behavior(
|
||||||
this.memberships$.pipe(
|
this.memberships$.pipe(
|
||||||
map((memberships) =>
|
map((memberships) =>
|
||||||
@@ -524,17 +512,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly joined$ = new Subject<void>();
|
private readonly join$ = new Subject<void>();
|
||||||
|
|
||||||
public join(): void {
|
public join(): void {
|
||||||
this.joined$.next();
|
this.join$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly leave$ = new Subject<void>();
|
||||||
|
|
||||||
public leave(): void {
|
public leave(): void {
|
||||||
// TODO
|
this.leave$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly connectionInstructions$ = this.joined$.pipe(
|
private readonly connectionInstructions$ = this.join$.pipe(
|
||||||
switchMap(() => this.remoteConnections$),
|
switchMap(() => this.remoteConnections$),
|
||||||
startWith(new Map<string, Connection>()),
|
startWith(new Map<string, Connection>()),
|
||||||
pairwise(),
|
pairwise(),
|
||||||
@@ -622,6 +612,17 @@ export class CallViewModel extends ViewModel {
|
|||||||
// in a split-brained state.
|
// in a split-brained state.
|
||||||
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
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
|
private readonly participants$ = this.scope
|
||||||
.behavior<
|
.behavior<
|
||||||
{
|
{
|
||||||
@@ -671,17 +672,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
)
|
)
|
||||||
.pipe(startWith([]), pauseWhen(this.pretendToBeDisconnected$));
|
.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
|
* Displaynames for each member of the call. This will disambiguate
|
||||||
* any displaynames that clashes with another member. Only members
|
* 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 start) void connection.start();
|
||||||
for (const connection of stop) connection.stop();
|
for (const connection of stop) connection.stop();
|
||||||
});
|
});
|
||||||
combineLatest([this.localFocus, this.joined$])
|
combineLatest([this.localFocus, this.join$])
|
||||||
.pipe(this.scope.bind())
|
.pipe(this.scope.bind())
|
||||||
.subscribe(([localFocus]) => {
|
.subscribe(([localFocus]) => {
|
||||||
void enterRTCSession(
|
void enterRTCSession(
|
||||||
@@ -1799,6 +1789,17 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.options.encryptionSystem.kind !== E2eeType.PER_PARTICIPANT,
|
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
|
// 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
|
// MatrixRTC, because it can be an unpleasant surprise for the app to say
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Config } from "../config/Config";
|
|||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
import { type ObservableScope } from "./ObservableScope";
|
import { type ObservableScope } from "./ObservableScope";
|
||||||
import { accumulate } from "../utils/observable";
|
import { accumulate } from "../utils/observable";
|
||||||
|
import { type Behavior } from "./Behavior";
|
||||||
|
|
||||||
interface MuteStateData {
|
interface MuteStateData {
|
||||||
enabled$: Observable<boolean>;
|
enabled$: Observable<boolean>;
|
||||||
@@ -33,13 +34,13 @@ interface MuteStateData {
|
|||||||
toggle: (() => void) | null;
|
toggle: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MuteState {
|
class MuteState<Label, Selected> {
|
||||||
private readonly enabledByDefault$ =
|
private readonly enabledByDefault$ =
|
||||||
this.enabledByConfig && !getUrlParams().skipLobby
|
this.enabledByConfig && !getUrlParams().skipLobby
|
||||||
? this.isJoined$.pipe(map((isJoined) => !isJoined))
|
? this.joined$.pipe(map((isJoined) => !isJoined))
|
||||||
: of(false);
|
: of(false);
|
||||||
|
|
||||||
private readonly data$: Observable<MuteStateData> =
|
private readonly data$ = this.scope.behavior<MuteStateData>(
|
||||||
this.device.available$.pipe(
|
this.device.available$.pipe(
|
||||||
map((available) => available.size > 0),
|
map((available) => available.size > 0),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@@ -52,8 +53,8 @@ class MuteState {
|
|||||||
const set$ = new Subject<boolean>();
|
const set$ = new Subject<boolean>();
|
||||||
const toggle$ = new Subject<void>();
|
const toggle$ = new Subject<void>();
|
||||||
return {
|
return {
|
||||||
set: (enabled: boolean) => set$.next(enabled),
|
set: (enabled: boolean): void => set$.next(enabled),
|
||||||
toggle: () => toggle$.next(),
|
toggle: (): void => toggle$.next(),
|
||||||
// Assume the default value only once devices are actually connected
|
// Assume the default value only once devices are actually connected
|
||||||
enabled$: merge(
|
enabled$: merge(
|
||||||
set$,
|
set$,
|
||||||
@@ -66,24 +67,24 @@ class MuteState {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
this.scope.state(),
|
),
|
||||||
);
|
|
||||||
|
|
||||||
public readonly enabled$: Observable<boolean> = this.data$.pipe(
|
|
||||||
switchMap(({ enabled$ }) => enabled$),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly setEnabled$: Observable<((enabled: boolean) => void) | null> =
|
public readonly enabled$: Behavior<boolean> = this.scope.behavior(
|
||||||
this.data$.pipe(map(({ set }) => set));
|
this.data$.pipe(switchMap(({ enabled$ }) => enabled$)),
|
||||||
|
);
|
||||||
|
|
||||||
public readonly toggle$: Observable<(() => void) | null> = this.data$.pipe(
|
public readonly setEnabled$: Behavior<((enabled: boolean) => void) | null> =
|
||||||
map(({ toggle }) => toggle),
|
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(
|
public constructor(
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
private readonly device: MediaDevice,
|
private readonly device: MediaDevice<Label, Selected>,
|
||||||
private readonly isJoined$: Observable<boolean>,
|
private readonly joined$: Observable<boolean>,
|
||||||
private readonly enabledByConfig: boolean,
|
private readonly enabledByConfig: boolean,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@@ -92,20 +93,20 @@ export class MuteStates {
|
|||||||
public readonly audio = new MuteState(
|
public readonly audio = new MuteState(
|
||||||
this.scope,
|
this.scope,
|
||||||
this.mediaDevices.audioInput,
|
this.mediaDevices.audioInput,
|
||||||
this.isJoined$,
|
this.joined$,
|
||||||
Config.get().media_devices.enable_video,
|
Config.get().media_devices.enable_video,
|
||||||
);
|
);
|
||||||
public readonly video = new MuteState(
|
public readonly video = new MuteState(
|
||||||
this.scope,
|
this.scope,
|
||||||
this.mediaDevices.videoInput,
|
this.mediaDevices.videoInput,
|
||||||
this.isJoined$,
|
this.joined$,
|
||||||
Config.get().media_devices.enable_video,
|
Config.get().media_devices.enable_video,
|
||||||
);
|
);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly scope: ObservableScope,
|
private readonly scope: ObservableScope,
|
||||||
private readonly mediaDevices: MediaDevices,
|
private readonly mediaDevices: MediaDevices,
|
||||||
private readonly isJoined$: Observable<boolean>,
|
private readonly joined$: Observable<boolean>,
|
||||||
) {
|
) {
|
||||||
if (widget !== null) {
|
if (widget !== null) {
|
||||||
// Sync our mute states with the hosting client
|
// Sync our mute states with the hosting client
|
||||||
|
|||||||
Reference in New Issue
Block a user