Refactor media devices to live outside React as Observables (#3334)

* Refactor media devices to live outside React as Observables

This moves the media devices state out of React to further our transition to a MVVM architecture in which we can more easily model and store complex application state. I have created an AppViewModel to act as the overarching state holder for any future non-React state we end up creating, and the MediaDevices reside within this. We should move more application logic (including the CallViewModel itself) there in the future.

* Address review feedback

* Fixes from ios debugging session: (#3342)

- dont use preferred vs selected concept in controlled media. Its not needed since we dont use the id for actual browser media devices (the id's are not even actual browser media devices)
  - add more logging
  - add more conditions to not accidently set a deviceId that is not a browser deviceId but one provided via controlled.

---------

Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
This commit is contained in:
Robin
2025-06-20 12:37:25 -04:00
committed by GitHub
parent 5bf7361d01
commit 5e2e94d794
24 changed files with 763 additions and 682 deletions

View File

@@ -21,15 +21,18 @@ import {
Separator,
} from "@vector-im/compound-web";
import { Trans, useTranslation } from "react-i18next";
import { useObservableEagerState } from "observable-hooks";
import {
EARPIECE_CONFIG_ID,
type MediaDeviceHandle,
} from "../livekit/MediaDevicesContext";
type AudioOutputDeviceLabel,
type DeviceLabel,
type SelectedDevice,
type MediaDevice,
} from "../state/MediaDevices";
import styles from "./DeviceSelection.module.css";
interface Props {
device: MediaDeviceHandle;
device: MediaDevice<DeviceLabel | AudioOutputDeviceLabel, SelectedDevice>;
title: string;
numberedLabel: (number: number) => string;
}
@@ -41,6 +44,8 @@ export const DeviceSelection: FC<Props> = ({
}) => {
const { t } = useTranslation();
const groupId = useId();
const available = useObservableEagerState(device.available$);
const selectedId = useObservableEagerState(device.selected$)?.id;
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
device.select(e.target.value);
@@ -49,7 +54,7 @@ export const DeviceSelection: FC<Props> = ({
);
// There is no need to show the menu if there is no choice that can be made.
if (device.available.size <= 1) return null;
if (available.size <= 1) return null;
return (
<div className={styles.selection}>
@@ -64,7 +69,7 @@ export const DeviceSelection: FC<Props> = ({
</Heading>
<Separator className={styles.separator} />
<div className={styles.options}>
{[...device.available].map(([id, label]) => {
{[...available].map(([id, label]) => {
let labelText: ReactNode;
switch (label.type) {
case "name":
@@ -94,20 +99,13 @@ export const DeviceSelection: FC<Props> = ({
break;
}
let isSelected = false;
if (device.useAsEarpiece) {
isSelected = id === EARPIECE_CONFIG_ID;
} else {
isSelected = id === device.selectedId;
}
return (
<InlineField
key={id}
name={groupId}
control={
<RadioControl
checked={isSelected}
checked={id === selectedId}
onChange={onChange}
value={id}
/>

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { type FC, type ReactNode, useState } from "react";
import { type FC, type ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { type MatrixClient } from "matrix-js-sdk";
import { Button, Root as Form, Separator } from "@vector-im/compound-web";
@@ -17,11 +17,8 @@ import styles from "./SettingsModal.module.css";
import { type Tab, TabContainer } from "../tabs/Tabs";
import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import {
useMediaDevices,
useMediaDeviceNames,
iosDeviceMenu$,
} from "../livekit/MediaDevicesContext";
import { iosDeviceMenu$ } from "../state/MediaDevices";
import { useMediaDevices } from "../MediaDevicesContext";
import { widget } from "../widget";
import {
useSetting,
@@ -98,7 +95,10 @@ export const SettingsModal: FC<Props> = ({
};
const devices = useMediaDevices();
useMediaDeviceNames(devices, open);
useEffect(() => {
if (open) devices.requestDeviceNames();
}, [open, devices]);
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume);
const [showDeveloperSettingsTab] = useSetting(developerMode);