mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-14 04:37:03 +00:00
Import unfinished mute states refactor
This commit is contained in:
163
src/state/MuteStates.ts
Normal file
163
src/state/MuteStates.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Copyright 2023-2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
fromEvent,
|
||||
map,
|
||||
merge,
|
||||
type Observable,
|
||||
of,
|
||||
Subject,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { type MediaDevices, type MediaDevice } from "../state/MediaDevices";
|
||||
import { ElementWidgetActions, widget } from "../widget";
|
||||
import { Config } from "../config/Config";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { type ObservableScope } from "./ObservableScope";
|
||||
import { accumulate } from "../utils/observable";
|
||||
|
||||
interface MuteStateData {
|
||||
enabled$: Observable<boolean>;
|
||||
set: ((enabled: boolean) => void) | null;
|
||||
toggle: (() => void) | null;
|
||||
}
|
||||
|
||||
class MuteState {
|
||||
private readonly enabledByDefault$ =
|
||||
this.enabledByConfig && !getUrlParams().skipLobby
|
||||
? this.isJoined$.pipe(map((isJoined) => !isJoined))
|
||||
: of(false);
|
||||
|
||||
private readonly data$: Observable<MuteStateData> =
|
||||
this.device.available$.pipe(
|
||||
map((available) => available.size > 0),
|
||||
distinctUntilChanged(),
|
||||
withLatestFrom(
|
||||
this.enabledByDefault$,
|
||||
(devicesConnected, enabledByDefault) => {
|
||||
if (!devicesConnected)
|
||||
return { enabled$: of(false), set: null, toggle: null };
|
||||
|
||||
const set$ = new Subject<boolean>();
|
||||
const toggle$ = new Subject<void>();
|
||||
return {
|
||||
set: (enabled: boolean) => set$.next(enabled),
|
||||
toggle: () => toggle$.next(),
|
||||
// Assume the default value only once devices are actually connected
|
||||
enabled$: merge(
|
||||
set$,
|
||||
toggle$.pipe(map(() => "toggle" as const)),
|
||||
).pipe(
|
||||
accumulate(enabledByDefault, (prev, update) =>
|
||||
update === "toggle" ? !prev : update,
|
||||
),
|
||||
),
|
||||
};
|
||||
},
|
||||
),
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
public readonly enabled$: Observable<boolean> = this.data$.pipe(
|
||||
switchMap(({ enabled$ }) => enabled$),
|
||||
);
|
||||
|
||||
public readonly setEnabled$: Observable<((enabled: boolean) => void) | null> =
|
||||
this.data$.pipe(map(({ set }) => set));
|
||||
|
||||
public readonly toggle$: Observable<(() => void) | null> = this.data$.pipe(
|
||||
map(({ toggle }) => toggle),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly scope: ObservableScope,
|
||||
private readonly device: MediaDevice,
|
||||
private readonly isJoined$: Observable<boolean>,
|
||||
private readonly enabledByConfig: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class MuteStates {
|
||||
public readonly audio = new MuteState(
|
||||
this.scope,
|
||||
this.mediaDevices.audioInput,
|
||||
this.isJoined$,
|
||||
Config.get().media_devices.enable_video,
|
||||
);
|
||||
public readonly video = new MuteState(
|
||||
this.scope,
|
||||
this.mediaDevices.videoInput,
|
||||
this.isJoined$,
|
||||
Config.get().media_devices.enable_video,
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly scope: ObservableScope,
|
||||
private readonly mediaDevices: MediaDevices,
|
||||
private readonly isJoined$: Observable<boolean>,
|
||||
) {
|
||||
if (widget !== null) {
|
||||
// Sync our mute states with the hosting client
|
||||
const widgetApiState$ = combineLatest(
|
||||
[this.audio.enabled$, this.video.enabled$],
|
||||
(audio, video) => ({ audio_enabled: audio, video_enabled: video }),
|
||||
);
|
||||
widgetApiState$.pipe(this.scope.bind()).subscribe((state) => {
|
||||
widget!.api.transport
|
||||
.send(ElementWidgetActions.DeviceMute, state)
|
||||
.catch((e) =>
|
||||
logger.warn("Could not send DeviceMute action to widget", e),
|
||||
);
|
||||
});
|
||||
|
||||
// Also sync the hosting client's mute states back with ours
|
||||
const muteActions$ = fromEvent(
|
||||
widget.lazyActions,
|
||||
ElementWidgetActions.DeviceMute,
|
||||
) as Observable<CustomEvent<IWidgetApiRequest>>;
|
||||
muteActions$
|
||||
.pipe(
|
||||
withLatestFrom(
|
||||
widgetApiState$,
|
||||
this.audio.setEnabled$,
|
||||
this.video.setEnabled$,
|
||||
),
|
||||
this.scope.bind(),
|
||||
)
|
||||
.subscribe(([ev, state, setAudioEnabled, setVideoEnabled]) => {
|
||||
// First copy the current state into our new state
|
||||
const newState = { ...state };
|
||||
// Update new state if there are any requested changes from the widget
|
||||
// action in `ev.detail.data`.
|
||||
if (
|
||||
ev.detail.data.audio_enabled != null &&
|
||||
typeof ev.detail.data.audio_enabled === "boolean" &&
|
||||
setAudioEnabled !== null
|
||||
) {
|
||||
newState.audio_enabled = ev.detail.data.audio_enabled;
|
||||
setAudioEnabled(newState.audio_enabled);
|
||||
}
|
||||
if (
|
||||
ev.detail.data.video_enabled != null &&
|
||||
typeof ev.detail.data.video_enabled === "boolean" &&
|
||||
setVideoEnabled !== null
|
||||
) {
|
||||
newState.video_enabled = ev.detail.data.video_enabled;
|
||||
setVideoEnabled(newState.video_enabled);
|
||||
}
|
||||
widget!.api.transport.reply(ev.detail, newState);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user