diff --git a/shared/js/ui/modal/settings/Microphone.tsx b/shared/js/ui/modal/settings/Microphone.tsx index 094d92f3..8ac653fd 100644 --- a/shared/js/ui/modal/settings/Microphone.tsx +++ b/shared/js/ui/modal/settings/Microphone.tsx @@ -19,10 +19,24 @@ export type MicrophoneSetting = export type MicrophoneDevice = { id: string, name: string, - driver: string + driver: string, + default: boolean }; - +export type MicrophoneSettingsSelectedMicrophone = { type: "default" } | { type: "none" } | { type: "device", deviceId: string }; +export type MicrophoneSettingsDevices = { + status: "error", + error: string +} | { + status: "audio-not-initialized" +} | { + status: "no-permissions", + shouldAsk: boolean +} | { + status: "success", + devices: MicrophoneDevice[] + selectedDevice: MicrophoneSettingsSelectedMicrophone; +}; export interface MicrophoneSettingsEvents { "query_devices": { refresh_list: boolean }, "query_help": {}, @@ -32,12 +46,13 @@ export interface MicrophoneSettingsEvents { "action_help_click": {}, "action_request_permissions": {}, - "action_set_selected_device": { deviceId: string }, + "action_set_selected_device": { target: MicrophoneSettingsSelectedMicrophone }, "action_set_selected_device_result": { - deviceId: string, /* on error it will contain the current selected device */ - status: "success" | "error", - - error?: string + status: "success", + selectedDevice: MicrophoneSettingsSelectedMicrophone + } | { + status: "error", + reason: string }, "action_set_setting": { @@ -50,15 +65,7 @@ export interface MicrophoneSettingsEvents { value: any; } - "notify_devices": { - status: "success" | "error" | "audio-not-initialized" | "no-permissions", - - error?: string, - shouldAsk?: boolean, - - devices?: MicrophoneDevice[] - selectedDevice?: string; - }, + "notify_devices": MicrophoneSettingsDevices, notify_device_level: { level: { @@ -166,7 +173,9 @@ export function initialize_audio_microphone_controller(events: Registry { if (!aplayer.initialized()) { - events.fire_react("notify_devices", {status: "audio-not-initialized"}); + events.fire_react("notify_devices", { + status: "audio-not-initialized" + }); return; } @@ -180,44 +189,92 @@ export function initialize_audio_microphone_controller(events: Registry { - }); + deviceList.refresh().then(() => { }); } else { const devices = deviceList.getDevices(); + let selectedDevice: MicrophoneSettingsSelectedMicrophone; + { + let deviceId = defaultRecorder.getDeviceId(); + if(deviceId === IDevice.DefaultDeviceId) { + selectedDevice = { type: "default" }; + } else if(deviceId === IDevice.NoDeviceId) { + selectedDevice = { type: "none" }; + } else { + selectedDevice = { type: "device", deviceId: deviceId }; + } + } + + const defaultDeviceId = getRecorderBackend().getDeviceList().getDefaultDeviceId(); events.fire_react("notify_devices", { status: "success", - selectedDevice: defaultRecorder.getDeviceId(), devices: devices.map(e => { - return {id: e.deviceId, name: e.name, driver: e.driver} - }) + return { + id: e.deviceId, + name: e.name, + driver: e.driver, + default: defaultDeviceId === e.deviceId + } + }), + selectedDevice: selectedDevice, }); } }); events.on("action_set_selected_device", event => { - const device = recorderBackend.getDeviceList().getDevices().find(e => e.deviceId === event.deviceId); - if (!device && event.deviceId !== IDevice.NoDeviceId) { - events.fire_react("action_set_selected_device_result", { - status: "error", - error: tr("Invalid device id"), - deviceId: defaultRecorder.getDeviceId() - }); - return; + let promise; + + const target = event.target; + + let displayName: string; + switch (target.type) { + case "none": + promise = defaultRecorder.setDevice("none"); + displayName = tr("No device"); + break; + + case "default": + promise = defaultRecorder.setDevice("default"); + displayName = tr("Default device"); + break; + + case "device": + const device = recorderBackend.getDeviceList().getDevices().find(e => e.deviceId === target.deviceId); + if (!device) { + events.fire_react("action_set_selected_device_result", { + status: "error", + reason: tr("Invalid device id"), + }); + return; + } + + displayName = target.deviceId; + promise = defaultRecorder.setDevice(device); + break; + + default: + events.fire_react("action_set_selected_device_result", { + status: "error", + reason: tr("Invalid device target"), + }); + return; + } - defaultRecorder.setDevice(device).then(() => { - logTrace(LogCategory.GENERAL, tr("Changed default microphone device to %s"), event.deviceId); - events.fire_react("action_set_selected_device_result", {status: "success", deviceId: event.deviceId}); + promise.then(() => { + logTrace(LogCategory.GENERAL, tr("Changed default microphone device to %s"), displayName); + events.fire_react("action_set_selected_device_result", {status: "success", selectedDevice: event.target }); }).catch((error) => { - logWarn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.deviceId : IDevice.NoDeviceId, error); - events.fire_react("action_set_selected_device_result", {status: "success", deviceId: event.deviceId}); + logWarn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), displayName, error); + events.fire_react("action_set_selected_device_result", {status: "error", reason: error || tr("lookup the console") }); }); }); } diff --git a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx index 0311485f..9893e7e7 100644 --- a/shared/js/ui/modal/settings/MicrophoneRenderer.tsx +++ b/shared/js/ui/modal/settings/MicrophoneRenderer.tsx @@ -3,7 +3,7 @@ import {useEffect, useRef, useState} from "react"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {Button} from "tc-shared/ui/react-elements/Button"; import {Registry} from "tc-shared/events"; -import {MicrophoneDevice, MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/Microphone"; +import {MicrophoneDevice, MicrophoneSettingsEvents, MicrophoneSettingsSelectedMicrophone} from "tc-shared/ui/modal/settings/Microphone"; import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; import {ClientIcon} from "svg-sprites/client-icons"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; @@ -43,22 +43,26 @@ type ActivityBarStatus = | { mode: "error", message: string } | { mode: "loading" } | { mode: "uninitialized" }; -const ActivityBar = (props: { events: Registry, deviceId: string, disabled?: boolean }) => { +const ActivityBar = (props: { events: Registry, deviceId: string | "none", disabled?: boolean }) => { const refHider = useRef(); - const [status, setStatus] = useState({mode: "loading"}); + const [status, setStatus] = useState({ mode: "loading" }); - if(typeof props.deviceId === "undefined") { throw "invalid device id"; } + if(typeof props.deviceId === "undefined") { + throw "invalid device id"; + } props.events.reactUse("notify_device_level", event => { if (event.status === "uninitialized") { - if (status.mode === "uninitialized") + if (status.mode === "uninitialized") { return; + } setStatus({mode: "uninitialized"}); } else if (event.status === "no-permissions") { const noPermissionsMessage = tr("no permissions"); - if (status.mode === "error" && status.message === noPermissionsMessage) + if (status.mode === "error" && status.message === noPermissionsMessage) { return; + } setStatus({mode: "error", message: noPermissionsMessage}); } else { @@ -73,10 +77,12 @@ const ActivityBar = (props: { events: Registry, device if (status.mode !== "success") { setStatus({mode: "success"}); } + refHider.current.style.width = (100 - device.level) + "%"; } else { - if (status.mode === "error" && status.message === device.error) + if (status.mode === "error" && status.message === device.error) { return; + } setStatus({mode: "error", message: device.error + ""}); } @@ -117,7 +123,7 @@ const Microphone = (props: { events: Registry, device:
-
{props.device.driver}
+
{props.device.driver + (props.device.default ? " (Default Device)" : "")}
{props.device.name}
@@ -167,7 +173,10 @@ const MicrophoneList = (props: { events: Registry }) = props.events.fire("query_devices"); return {type: "loading"}; }); - const [selectedDevice, setSelectedDevice] = useState<{ deviceId: string, mode: "selected" | "selecting" }>(); + const [selectedDevice, setSelectedDevice] = useState<{ + selectedDevice: MicrophoneSettingsSelectedMicrophone, + selectingDevice: MicrophoneSettingsSelectedMicrophone | undefined + }>(); const [deviceList, setDeviceList] = useState([]); props.events.reactUse("notify_devices", event => { @@ -176,7 +185,10 @@ const MicrophoneList = (props: { events: Registry }) = case "success": setDeviceList(event.devices.slice(0)); setState({type: "normal"}); - setSelectedDevice({mode: "selected", deviceId: event.selectedDevice}); + setSelectedDevice({ + selectedDevice: event.selectedDevice, + selectingDevice: undefined + }); break; case "error": @@ -194,16 +206,49 @@ const MicrophoneList = (props: { events: Registry }) = }); props.events.reactUse("action_set_selected_device", event => { - setSelectedDevice({mode: "selecting", deviceId: event.deviceId}); + setSelectedDevice({ + selectedDevice: selectedDevice?.selectedDevice, + selectingDevice: event.target + }); }); props.events.reactUse("action_set_selected_device_result", event => { - if (event.status === "error") - createErrorModal(tr("Failed to select microphone"), tra("Failed to select microphone:\n{}", event.error)).open(); - - setSelectedDevice({mode: "selected", deviceId: event.deviceId}); + if (event.status === "error") { + createErrorModal(tr("Failed to select microphone"), tra("Failed to select microphone:\n{}", event.reason)).open(); + setSelectedDevice({ + selectedDevice: selectedDevice?.selectedDevice, + selectingDevice: undefined + }); + } else { + setSelectedDevice({ + selectedDevice: event.selectedDevice, + selectingDevice: undefined + }); + } }); + const deviceSelectState = (device: MicrophoneDevice | "none" | "default"): MicrophoneSelectedState => { + let selected: MicrophoneSettingsSelectedMicrophone; + let mode: MicrophoneSelectedState; + if(typeof selectedDevice?.selectingDevice !== "undefined") { + selected = selectedDevice.selectingDevice; + mode = "applying"; + } else if(typeof selectedDevice?.selectedDevice !== "undefined") { + selected = selectedDevice.selectedDevice; + mode = "selected"; + } else { + return "unselected"; + } + + if(selected.type === "default") { + return device === "default" || (typeof device === "object" && device.default) ? mode : "unselected"; + } else if(selected.type === "none") { + return device === "none" ? mode : "unselected"; + } else { + return typeof device === "object" && device.id === selected.deviceId ? mode : "unselected"; + } + } + return (
}) = - { - if (state.type !== "normal" || selectedDevice?.mode === "selecting") + if (state.type !== "normal" || selectedDevice?.selectingDevice) { return; + } - props.events.fire("action_set_selected_device", {deviceId: IDevice.NoDeviceId}); + props.events.fire("action_set_selected_device", { target: { type: "none" } }); }} /> - {deviceList.map(e => { - if (state.type !== "normal" || selectedDevice?.mode === "selecting") + if (state.type !== "normal" || selectedDevice?.selectingDevice) { return; + } - props.events.fire("action_set_selected_device", {deviceId: e.id}); + if(device.default) { + props.events.fire("action_set_selected_device", { target: { type: "default" } }); + } else { + props.events.fire("action_set_selected_device", { target: { type: "device", deviceId: device.id } }); + } }} />)}
@@ -509,30 +565,64 @@ const ThresholdSelector = (props: { events: Registry } return "loading"; }); - const [currentDevice, setCurrentDevice] = useState(undefined); - const [isActive, setActive] = useState(false); + const [currentDevice, setCurrentDevice] = useState<{ type: "none" } | { type: "device", deviceId: string }>({ type: "none" }); + const defaultDeviceId = useRef(); + const [isVadActive, setVadActive] = useState(false); + + const changeCurrentDevice = (selected: MicrophoneSettingsSelectedMicrophone) => { + switch (selected.type) { + case "none": + setCurrentDevice({ type: "none" }); + break; + + case "device": + setCurrentDevice({ type: "device", deviceId: selected.deviceId }); + break; + + case "default": + if(defaultDeviceId.current) { + setCurrentDevice({ type: "device", deviceId: defaultDeviceId.current }); + } else { + setCurrentDevice({ type: "none" }); + } + break; + + default: + throw tr("invalid device type"); + } + } props.events.reactUse("notify_setting", event => { if (event.setting === "threshold-threshold") { refSlider.current?.setState({value: event.value}); setValue(event.value); } else if (event.setting === "vad-type") { - setActive(event.value === "threshold"); + setVadActive(event.value === "threshold"); } }); props.events.reactUse("notify_devices", event => { - setCurrentDevice(event.selectedDevice); + if(event.status === "success") { + const defaultDevice = event.devices.find(device => device.default); + defaultDeviceId.current = defaultDevice?.id; + changeCurrentDevice(event.selectedDevice); + } else { + defaultDeviceId.current = undefined; + setCurrentDevice({ type: "none" }); + } }); props.events.reactUse("action_set_selected_device_result", event => { - setCurrentDevice(event.deviceId); + if(event.status === "success") { + changeCurrentDevice(event.selectedDevice); + } }); + let isActive = isVadActive && currentDevice.type === "device"; return (
- +