Improved microphone setting dialog

This commit is contained in:
WolverinDEV 2021-02-15 17:02:53 +01:00
parent e5ed31fb47
commit ddef3359bd
2 changed files with 215 additions and 68 deletions

View file

@ -19,10 +19,24 @@ export type MicrophoneSetting =
export type MicrophoneDevice = { export type MicrophoneDevice = {
id: string, id: string,
name: 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 { export interface MicrophoneSettingsEvents {
"query_devices": { refresh_list: boolean }, "query_devices": { refresh_list: boolean },
"query_help": {}, "query_help": {},
@ -32,12 +46,13 @@ export interface MicrophoneSettingsEvents {
"action_help_click": {}, "action_help_click": {},
"action_request_permissions": {}, "action_request_permissions": {},
"action_set_selected_device": { deviceId: string }, "action_set_selected_device": { target: MicrophoneSettingsSelectedMicrophone },
"action_set_selected_device_result": { "action_set_selected_device_result": {
deviceId: string, /* on error it will contain the current selected device */ status: "success",
status: "success" | "error", selectedDevice: MicrophoneSettingsSelectedMicrophone
} | {
error?: string status: "error",
reason: string
}, },
"action_set_setting": { "action_set_setting": {
@ -50,15 +65,7 @@ export interface MicrophoneSettingsEvents {
value: any; value: any;
} }
"notify_devices": { "notify_devices": MicrophoneSettingsDevices,
status: "success" | "error" | "audio-not-initialized" | "no-permissions",
error?: string,
shouldAsk?: boolean,
devices?: MicrophoneDevice[]
selectedDevice?: string;
},
notify_device_level: { notify_device_level: {
level: { level: {
@ -166,7 +173,9 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
{ {
events.on("query_devices", event => { events.on("query_devices", event => {
if (!aplayer.initialized()) { if (!aplayer.initialized()) {
events.fire_react("notify_devices", {status: "audio-not-initialized"}); events.fire_react("notify_devices", {
status: "audio-not-initialized"
});
return; return;
} }
@ -180,44 +189,92 @@ export function initialize_audio_microphone_controller(events: Registry<Micropho
return; return;
case "uninitialized": case "uninitialized":
events.fire_react("notify_devices", {status: "audio-not-initialized"}); events.fire_react("notify_devices", {
status: "audio-not-initialized"
});
return; return;
} }
if (event.refresh_list && deviceList.isRefreshAvailable()) { if (event.refresh_list && deviceList.isRefreshAvailable()) {
/* will automatically trigger a device list changed event if something has changed */ /* will automatically trigger a device list changed event if something has changed */
deviceList.refresh().then(() => { deviceList.refresh().then(() => { });
});
} else { } else {
const devices = deviceList.getDevices(); 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", { events.fire_react("notify_devices", {
status: "success", status: "success",
selectedDevice: defaultRecorder.getDeviceId(),
devices: devices.map(e => { 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 => { events.on("action_set_selected_device", event => {
const device = recorderBackend.getDeviceList().getDevices().find(e => e.deviceId === event.deviceId); let promise;
if (!device && event.deviceId !== IDevice.NoDeviceId) {
events.fire_react("action_set_selected_device_result", { const target = event.target;
status: "error",
error: tr("Invalid device id"), let displayName: string;
deviceId: defaultRecorder.getDeviceId() switch (target.type) {
}); case "none":
return; 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(() => { promise.then(() => {
logTrace(LogCategory.GENERAL, tr("Changed default microphone device to %s"), event.deviceId); logTrace(LogCategory.GENERAL, tr("Changed default microphone device to %s"), displayName);
events.fire_react("action_set_selected_device_result", {status: "success", deviceId: event.deviceId}); events.fire_react("action_set_selected_device_result", {status: "success", selectedDevice: event.target });
}).catch((error) => { }).catch((error) => {
logWarn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), device ? device.deviceId : IDevice.NoDeviceId, error); logWarn(LogCategory.AUDIO, tr("Failed to change microphone to device %s: %o"), displayName, error);
events.fire_react("action_set_selected_device_result", {status: "success", deviceId: event.deviceId}); events.fire_react("action_set_selected_device_result", {status: "error", reason: error || tr("lookup the console") });
}); });
}); });
} }

View file

@ -3,7 +3,7 @@ import {useEffect, useRef, useState} from "react";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {Button} from "tc-shared/ui/react-elements/Button"; import {Button} from "tc-shared/ui/react-elements/Button";
import {Registry} from "tc-shared/events"; 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 {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {ClientIcon} from "svg-sprites/client-icons"; import {ClientIcon} from "svg-sprites/client-icons";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
@ -43,22 +43,26 @@ type ActivityBarStatus =
| { mode: "error", message: string } | { mode: "error", message: string }
| { mode: "loading" } | { mode: "loading" }
| { mode: "uninitialized" }; | { mode: "uninitialized" };
const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string, disabled?: boolean }) => { const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string | "none", disabled?: boolean }) => {
const refHider = useRef<HTMLDivElement>(); const refHider = useRef<HTMLDivElement>();
const [status, setStatus] = useState<ActivityBarStatus>({mode: "loading"}); const [status, setStatus] = useState<ActivityBarStatus>({ 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 => { props.events.reactUse("notify_device_level", event => {
if (event.status === "uninitialized") { if (event.status === "uninitialized") {
if (status.mode === "uninitialized") if (status.mode === "uninitialized") {
return; return;
}
setStatus({mode: "uninitialized"}); setStatus({mode: "uninitialized"});
} else if (event.status === "no-permissions") { } else if (event.status === "no-permissions") {
const noPermissionsMessage = tr("no permissions"); const noPermissionsMessage = tr("no permissions");
if (status.mode === "error" && status.message === noPermissionsMessage) if (status.mode === "error" && status.message === noPermissionsMessage) {
return; return;
}
setStatus({mode: "error", message: noPermissionsMessage}); setStatus({mode: "error", message: noPermissionsMessage});
} else { } else {
@ -73,10 +77,12 @@ const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, device
if (status.mode !== "success") { if (status.mode !== "success") {
setStatus({mode: "success"}); setStatus({mode: "success"});
} }
refHider.current.style.width = (100 - device.level) + "%"; refHider.current.style.width = (100 - device.level) + "%";
} else { } else {
if (status.mode === "error" && status.message === device.error) if (status.mode === "error" && status.message === device.error) {
return; return;
}
setStatus({mode: "error", message: device.error + ""}); setStatus({mode: "error", message: device.error + ""});
} }
@ -117,7 +123,7 @@ const Microphone = (props: { events: Registry<MicrophoneSettingsEvents>, device:
<MicrophoneStatus state={props.state}/> <MicrophoneStatus state={props.state}/>
</div> </div>
<div className={cssStyle.containerName}> <div className={cssStyle.containerName}>
<div className={cssStyle.driver}>{props.device.driver}</div> <div className={cssStyle.driver}>{props.device.driver + (props.device.default ? " (Default Device)" : "")}</div>
<div className={cssStyle.name}>{props.device.name}</div> <div className={cssStyle.name}>{props.device.name}</div>
</div> </div>
<div className={cssStyle.containerActivity}> <div className={cssStyle.containerActivity}>
@ -167,7 +173,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
props.events.fire("query_devices"); props.events.fire("query_devices");
return {type: "loading"}; 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<MicrophoneDevice[]>([]); const [deviceList, setDeviceList] = useState<MicrophoneDevice[]>([]);
props.events.reactUse("notify_devices", event => { props.events.reactUse("notify_devices", event => {
@ -176,7 +185,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
case "success": case "success":
setDeviceList(event.devices.slice(0)); setDeviceList(event.devices.slice(0));
setState({type: "normal"}); setState({type: "normal"});
setSelectedDevice({mode: "selected", deviceId: event.selectedDevice}); setSelectedDevice({
selectedDevice: event.selectedDevice,
selectingDevice: undefined
});
break; break;
case "error": case "error":
@ -194,16 +206,49 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
}); });
props.events.reactUse("action_set_selected_device", event => { 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 => { props.events.reactUse("action_set_selected_device_result", event => {
if (event.status === "error") if (event.status === "error") {
createErrorModal(tr("Failed to select microphone"), tra("Failed to select microphone:\n{}", event.error)).open(); createErrorModal(tr("Failed to select microphone"), tra("Failed to select microphone:\n{}", event.reason)).open();
setSelectedDevice({
setSelectedDevice({mode: "selected", deviceId: event.deviceId}); 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 ( return (
<div className={cssStyle.body + " " + cssStyle.containerDevices}> <div className={cssStyle.body + " " + cssStyle.containerDevices}>
<div <div
@ -232,28 +277,39 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
<div className={cssStyle.overlay + " " + (state.type !== "loading" ? cssStyle.hidden : undefined)}> <div className={cssStyle.overlay + " " + (state.type !== "loading" ? cssStyle.hidden : undefined)}>
<a><Translatable>Loading</Translatable>&nbsp;<LoadingDots/></a> <a><Translatable>Loading</Translatable>&nbsp;<LoadingDots/></a>
</div> </div>
<Microphone key={"d-default"} <Microphone key={"d-no-device"}
device={{id: IDevice.NoDeviceId, driver: tr("No device"), name: tr("No device")}} device={{
id: "none",
driver: tr("No device"),
name: tr("No device"),
default: false
}}
events={props.events} events={props.events}
state={IDevice.NoDeviceId === selectedDevice?.deviceId ? selectedDevice.mode === "selecting" ? "applying" : "selected" : "unselected"} state={deviceSelectState("none")}
onClick={() => { onClick={() => {
if (state.type !== "normal" || selectedDevice?.mode === "selecting") if (state.type !== "normal" || selectedDevice?.selectingDevice) {
return; return;
}
props.events.fire("action_set_selected_device", {deviceId: IDevice.NoDeviceId}); props.events.fire("action_set_selected_device", { target: { type: "none" } });
}} }}
/> />
{deviceList.map(e => <Microphone {deviceList.map(device => <Microphone
key={"d-" + e.id} key={"d-" + device.id}
device={e} device={device}
events={props.events} events={props.events}
state={e.id === selectedDevice?.deviceId ? selectedDevice.mode === "selecting" ? "applying" : "selected" : "unselected"} state={deviceSelectState(device)}
onClick={() => { onClick={() => {
if (state.type !== "normal" || selectedDevice?.mode === "selecting") if (state.type !== "normal" || selectedDevice?.selectingDevice) {
return; 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 } });
}
}} }}
/>)} />)}
</div> </div>
@ -509,30 +565,64 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
return "loading"; return "loading";
}); });
const [currentDevice, setCurrentDevice] = useState(undefined); const [currentDevice, setCurrentDevice] = useState<{ type: "none" } | { type: "device", deviceId: string }>({ type: "none" });
const [isActive, setActive] = useState(false); const defaultDeviceId = useRef<string | undefined>();
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 => { props.events.reactUse("notify_setting", event => {
if (event.setting === "threshold-threshold") { if (event.setting === "threshold-threshold") {
refSlider.current?.setState({value: event.value}); refSlider.current?.setState({value: event.value});
setValue(event.value); setValue(event.value);
} else if (event.setting === "vad-type") { } else if (event.setting === "vad-type") {
setActive(event.value === "threshold"); setVadActive(event.value === "threshold");
} }
}); });
props.events.reactUse("notify_devices", event => { 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 => { 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 ( return (
<div className={cssStyle.containerSensitivity}> <div className={cssStyle.containerSensitivity}>
<div className={cssStyle.containerBar}> <div className={cssStyle.containerBar}>
<ActivityBar events={props.events} deviceId={currentDevice || "none"} disabled={!isActive || !currentDevice} key={"activity-" + currentDevice} /> <ActivityBar events={props.events} deviceId={currentDevice.type === "device" ? currentDevice.deviceId : "none"} disabled={!isActive || !currentDevice} key={"activity-" + currentDevice} />
</div> </div>
<Slider <Slider
ref={refSlider} ref={refSlider}