Improved microphone setting dialog
parent
e5ed31fb47
commit
ddef3359bd
|
@ -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<Micropho
|
|||
{
|
||||
events.on("query_devices", event => {
|
||||
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<Micropho
|
|||
return;
|
||||
|
||||
case "uninitialized":
|
||||
events.fire_react("notify_devices", {status: "audio-not-initialized"});
|
||||
events.fire_react("notify_devices", {
|
||||
status: "audio-not-initialized"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.refresh_list && deviceList.isRefreshAvailable()) {
|
||||
/* will automatically trigger a device list changed event if something has changed */
|
||||
deviceList.refresh().then(() => {
|
||||
});
|
||||
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") });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<MicrophoneSettingsEvents>, deviceId: string, disabled?: boolean }) => {
|
||||
const ActivityBar = (props: { events: Registry<MicrophoneSettingsEvents>, deviceId: string | "none", disabled?: boolean }) => {
|
||||
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 => {
|
||||
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<MicrophoneSettingsEvents>, 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<MicrophoneSettingsEvents>, device:
|
|||
<MicrophoneStatus state={props.state}/>
|
||||
</div>
|
||||
<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>
|
||||
<div className={cssStyle.containerActivity}>
|
||||
|
@ -167,7 +173,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
|||
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<MicrophoneDevice[]>([]);
|
||||
|
||||
props.events.reactUse("notify_devices", event => {
|
||||
|
@ -176,7 +185,10 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
|||
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<MicrophoneSettingsEvents> }) =
|
|||
});
|
||||
|
||||
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 (
|
||||
<div className={cssStyle.body + " " + cssStyle.containerDevices}>
|
||||
<div
|
||||
|
@ -232,28 +277,39 @@ const MicrophoneList = (props: { events: Registry<MicrophoneSettingsEvents> }) =
|
|||
<div className={cssStyle.overlay + " " + (state.type !== "loading" ? cssStyle.hidden : undefined)}>
|
||||
<a><Translatable>Loading</Translatable> <LoadingDots/></a>
|
||||
</div>
|
||||
<Microphone key={"d-default"}
|
||||
device={{id: IDevice.NoDeviceId, driver: tr("No device"), name: tr("No device")}}
|
||||
<Microphone key={"d-no-device"}
|
||||
device={{
|
||||
id: "none",
|
||||
driver: tr("No device"),
|
||||
name: tr("No device"),
|
||||
default: false
|
||||
}}
|
||||
events={props.events}
|
||||
state={IDevice.NoDeviceId === selectedDevice?.deviceId ? selectedDevice.mode === "selecting" ? "applying" : "selected" : "unselected"}
|
||||
state={deviceSelectState("none")}
|
||||
onClick={() => {
|
||||
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 => <Microphone
|
||||
key={"d-" + e.id}
|
||||
device={e}
|
||||
{deviceList.map(device => <Microphone
|
||||
key={"d-" + device.id}
|
||||
device={device}
|
||||
events={props.events}
|
||||
state={e.id === selectedDevice?.deviceId ? selectedDevice.mode === "selecting" ? "applying" : "selected" : "unselected"}
|
||||
state={deviceSelectState(device)}
|
||||
onClick={() => {
|
||||
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 } });
|
||||
}
|
||||
}}
|
||||
/>)}
|
||||
</div>
|
||||
|
@ -509,30 +565,64 @@ const ThresholdSelector = (props: { events: Registry<MicrophoneSettingsEvents> }
|
|||
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<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 => {
|
||||
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 (
|
||||
<div className={cssStyle.containerSensitivity}>
|
||||
<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>
|
||||
<Slider
|
||||
ref={refSlider}
|
||||
|
|
Loading…
Reference in New Issue