diff --git a/locales/en/app.json b/locales/en/app.json index 5398930f..0b1fb776 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -201,7 +201,6 @@ "camera_numbered": "Camera {{n}}", "change_device_button": "Change audio device", "default": "Default", - "default_named": "Default <2>({{name}})", "handset": "Handset", "loudspeaker": "Loudspeaker", "microphone": "Microphone", diff --git a/src/components/CallFooter.tsx b/src/components/CallFooter.tsx index af4ad478..efea2ce1 100644 --- a/src/components/CallFooter.tsx +++ b/src/components/CallFooter.tsx @@ -7,7 +7,8 @@ Please see LICENSE in the repository root for full details. import { type FC, type JSX, type Ref, useMemo } from "react"; import classNames from "classnames"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; +import { useObservableEagerState } from "observable-hooks"; import LogoMark from "../icons/LogoMark.svg?react"; import LogoType from "../icons/LogoType.svg?react"; @@ -29,7 +30,18 @@ import { MediaMuteAndSwitchButton, type MenuOptions, } from "./MediaMuteAndSwitchButton"; - +import { + type AudioOutputDeviceLabel, + type DeviceLabel, + type MediaDevice, + type SelectedDevice, +} from "../state/MediaDevices"; +import { mediaDeviceLabelToString } from "../settings/DeviceSelection"; +import { + backgroundBlur as backgroundBlurSettings, + useSetting, +} from "../settings/settings"; +import { useTrackProcessor } from "../livekit/TrackProcessorContext"; export interface AudioOutputSwitcher { targetOutput: string; switch: () => void; @@ -85,6 +97,19 @@ export interface FooterProps { selectedVideo?: string; selectAudioDevice?: (deviceId: string) => void; selectVideoDevice?: (deviceId: string) => void; + /** + * If provided the footer will use the switchAndMute buttons. + * If not provided it will use the normal mute Buttons + */ + audioDevice?: MediaDevice< + DeviceLabel | AudioOutputDeviceLabel, + SelectedDevice + >; + /** + * If provided the footer will use the switchAndMute buttons. + * If not provided it will use the normal mute Buttons + */ + videoDevice?: MediaDevice; } export const CallFooter: FC = ({ @@ -111,13 +136,25 @@ export const CallFooter: FC = ({ debugTileLayout, tileStoreGeneration, - audioOptions, - videoOptions, - selectedAudio, - selectedVideo, - selectAudioDevice, - selectVideoDevice, + audioDevice, + videoDevice, }) => { + const videoOptions = useObservableEagerState( + videoDevice?.available$ ?? of(new Map()), + ); + const selectedVideo = useObservableEagerState( + videoDevice?.selected$ ?? of(undefined), + ); + const audioOptions = useObservableEagerState( + audioDevice?.available$ ?? of(new Map()), + ); + const selectedAudio = useObservableEagerState( + audioDevice?.selected$ ?? of(undefined), + ); + + const { supported: blurSupported } = useTrackProcessor(); + const [blurActive, setBlurActive] = useSetting(backgroundBlurSettings); + const buttons: JSX.Element[] = []; const buttonSize = asPip ? "md" : "lg"; const showSettingsButton = @@ -138,18 +175,24 @@ export const CallFooter: FC = ({ ); } - if ((audioOptions?.length ?? 0) > 0) { + if ((audioOptions?.size ?? 0) > 0) { buttons.push( { + const label = mediaDeviceLabelToString(v, (n) => "Audio Device " + n); + return { + id: k, + label: label, + }; + })} + selectedOption={selectedAudio?.id} + onSelect={audioDevice?.select} />, ); } else { @@ -164,18 +207,40 @@ export const CallFooter: FC = ({ />, ); } - if ((videoOptions?.length ?? 0) > 0) { + if ((videoOptions?.size ?? 0) > 0) { buttons.push( ({ + id: k, + label: v.type === "name" ? v.name : "Camera " + v.number, + }))} + toggles={ + blurSupported + ? [ + { + id: "blur", + enabled: blurActive, + label: "Blur Background", + }, + ] + : [] + } + selectedOption={selectedVideo?.id} + onSelect={(option) => { + switch (option) { + case "blur": + setBlurActive(!blurActive); + break; + default: + videoDevice?.select(option); + } + }} />, ); } else { diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7fc97e27..ae4b880c 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -220,7 +220,7 @@ export const InCallView: FC = ({ muted: muteAllAudio, }); const latestPickupPhaseAudio = useLatest(pickupPhaseAudio); - + const mediaDevices = useMediaDevices(); const audioEnabled = useBehavior(muteStates.audio.enabled$); const videoEnabled = useBehavior(muteStates.video.enabled$); const toggleAudio = useBehavior(muteStates.audio.toggle$); @@ -597,6 +597,8 @@ export const InCallView: FC = ({ //Debug props debugTileLayout={debugTileLayout} tileStoreGeneration={tileStoreGeneration} + audioDevice={mediaDevices.audioInput} + videoDevice={mediaDevices.videoInput} /> ); const allConnections = useBehavior(vm.allConnections$); diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 277ed61c..529332e8 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -236,6 +236,8 @@ export const LobbyView: FC = ({ hangup={!confineToRoom ? onLeaveClick : undefined} // Logo and header are connected. We will only show the logo in SPA with header. hideLogo={hideHeader} + audioDevice={devices.audioInput} + videoDevice={devices.videoInput} > {recentsButtonInFooter && recentsButton} diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index 197046c3..f189348b 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -5,14 +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 ChangeEvent, - type FC, - type ReactElement, - type ReactNode, - useCallback, - useId, -} from "react"; +import { type ChangeEvent, type FC, useCallback, useId } from "react"; import { Heading, InlineField, @@ -20,8 +13,8 @@ import { RadioControl, Separator, } from "@vector-im/compound-web"; -import { Trans, useTranslation } from "react-i18next"; import { useObservableEagerState } from "observable-hooks"; +import { t } from "i18next"; import { type AudioOutputDeviceLabel, @@ -37,12 +30,39 @@ interface Props { numberedLabel: (number: number) => string; } +export function mediaDeviceLabelToString( + label: DeviceLabel | AudioOutputDeviceLabel, + numberedLabel: (number: number) => string, +): string { + let labelText = ""; + switch (label.type) { + case "name": + labelText = label.name; + break; + case "number": + labelText = numberedLabel(label.number); + break; + case "default": + labelText = + label.name === null + ? t("settings.devices.default") + : t("settings.devices.default") + " (" + label.name + ")"; + break; + case "speaker": + labelText = t("settings.devices.loudspeaker"); + break; + case "earpiece": + labelText = t("settings.devices.handset"); + break; + } + return labelText; +} + export const DeviceSelection: FC = ({ device, title, numberedLabel, }) => { - const { t } = useTranslation(); const groupId = useId(); const available = useObservableEagerState(device.available$); const selectedId = useObservableEagerState(device.selected$)?.id; @@ -70,38 +90,7 @@ export const DeviceSelection: FC = ({
{[...available].map(([id, label]) => { - let labelText: ReactNode; - switch (label.type) { - case "name": - labelText = label.name; - break; - case "number": - labelText = numberedLabel(label.number); - break; - case "default": - labelText = - label.name === null ? ( - t("settings.devices.default") - ) : ( - - Default{" "} - - ({{ name: label.name } as unknown as ReactElement}) - - - ); - break; - case "speaker": - labelText = t("settings.devices.loudspeaker"); - break; - case "earpiece": - labelText = t("settings.devices.handset"); - break; - } - + const labelText = mediaDeviceLabelToString(label, numberedLabel); return (