TeaWeb/shared/js/ui/modal/settings/Microphone.tsx
2021-04-05 23:05:44 +02:00

464 lines
No EOL
18 KiB
TypeScript

import * as React from "react";
import {Registry} from "tc-shared/events";
import {AbstractInput, FilterMode, LevelMeter} from "tc-shared/voice/RecorderBase";
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log";
import {ConnectionRecorderProfileOwner, defaultRecorder, RecorderProfileOwner} from "tc-shared/voice/RecorderProfile";
import {getRecorderBackend, InputDevice} from "tc-shared/audio/Recorder";
import {Settings, settings} from "tc-shared/settings";
import {getBackend} from "tc-shared/backend";
import * as _ from "lodash";
import {getAudioBackend} from "tc-shared/audio/Player";
import {
InputDeviceLevel,
MicrophoneSettingsEvents,
SelectedMicrophone
} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
import {spawnInputProcessorModal} from "tc-shared/ui/modal/input-processor/Controller";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
import {server_connections} from "tc-shared/ConnectionManager";
import {ignorePromise} from "tc-shared/proto";
export function initialize_audio_microphone_controller(events: Registry<MicrophoneSettingsEvents>) {
const recorderBackend = getRecorderBackend();
/* level meters */
{
const levelMeterInitializePromises: { [key: string]: Promise<LevelMeter> } = {};
const deviceLevelInfo: { [key: string]: any } = {};
let deviceLevelUpdateTask;
let selectedDevice: SelectedMicrophone = { type: "none" };
const destroyLevelIndicators = () => {
Object.keys(levelMeterInitializePromises).forEach(e => {
const meter = levelMeterInitializePromises[e];
delete levelMeterInitializePromises[e];
meter.then(e => e.destroy());
});
Object.keys(deviceLevelInfo).forEach(e => delete deviceLevelInfo[e]);
};
const updateLevelMeter = () => {
destroyLevelIndicators();
let levelMeterEnabled;
{
let defaultValue = true;
if(__build.target === "client" && getBackend("native").getVersionInfo().os_platform === "linux") {
/* The linux client crashes when it fails to open an alsa stream due too many opened streams */
defaultValue = false;
}
levelMeterEnabled = settings.getValue(Settings.KEY_MICROPHONE_LEVEL_INDICATOR, defaultValue);
}
deviceLevelInfo["none"] = {deviceId: "none", status: "success", level: 0};
const defaultDeviceId = recorderBackend.getDeviceList().getDefaultDeviceId();
for (const device of recorderBackend.getDeviceList().getDevices()) {
let createLevelMeter;
if(!levelMeterEnabled) {
switch (selectedDevice.type) {
case "default":
createLevelMeter = device.deviceId == defaultDeviceId;
break;
case "device":
createLevelMeter = device.deviceId == selectedDevice.deviceId;
break;
case "none":
createLevelMeter = false;
break;
}
} else {
createLevelMeter = true;
}
if(createLevelMeter) {
let promise = recorderBackend.createLevelMeter(device).then(meter => {
meter.setObserver(level => {
if (levelMeterInitializePromises[device.deviceId] !== promise) {
/* old level meter */
return;
}
deviceLevelInfo[device.deviceId] = {
deviceId: device.deviceId,
status: "success",
level: level
};
});
return Promise.resolve(meter);
}).catch(error => {
if (levelMeterInitializePromises[device.deviceId] !== promise) {
/* old level meter */
return;
}
deviceLevelInfo[device.deviceId] = {
deviceId: device.deviceId,
status: "error",
error: error
};
logWarn(LogCategory.AUDIO, tr("Failed to initialize a level meter for device %s (%s): %o"), device.deviceId, device.driver + ":" + device.name, error);
return Promise.reject(error);
});
levelMeterInitializePromises[device.deviceId] = promise;
} else {
deviceLevelInfo[device.deviceId] = {
deviceId: device.deviceId,
status: "error",
error: tr("level meter disabled")
};
}
}
};
deviceLevelUpdateTask = setInterval(() => {
const deviceListStatus = recorderBackend.getDeviceList().getStatus();
events.fire("notify_device_level", {
level: deviceLevelInfo,
status: deviceListStatus === "error" ? "uninitialized" : deviceListStatus
});
}, 50);
events.on("notify_devices", event => {
if (event.status !== "success") {
return;
}
selectedDevice = event.selectedDevice;
updateLevelMeter();
});
events.on("notify_destroy", () => {
destroyLevelIndicators();
clearInterval(deviceLevelUpdateTask);
});
events.on("notify_device_selected", event => {
if(_.isEqual(selectedDevice, event.device)) {
return;
}
selectedDevice = event.device;
updateLevelMeter();
});
}
{
let currentLevel: InputDeviceLevel = { status: "uninitialized" };
let levelMeter: LevelMeter;
/* input device level meter */
const initializeInput = (input: AbstractInput) => {
try {
levelMeter?.destroy();
levelMeter = input.createLevelMeter();
levelMeter.setObserver(value => {
currentLevel = { status: "success", level: value };
events.fire_react("notify_input_level", { level: currentLevel });
});
currentLevel = { status: "success", level: 0 };
} catch (error) {
if(typeof error !== "string") {
logError(LogCategory.GENERAL, tr("Failed to create input device level meter: %o"), error);
error = tr("lookup the console");
}
currentLevel = { status: "error", message: error };
}
events.fire_react("notify_input_level", { level: currentLevel });
}
events.on("notify_destroy", () => {
levelMeter?.setObserver(undefined);
levelMeter?.destroy();
});
events.on("query_input_level", () => events.fire_react("notify_input_level", { level: currentLevel }));
if(defaultRecorder.input) {
initializeInput(defaultRecorder.input);
} else {
events.on("notify_destroy", defaultRecorder.events.one("notify_input_initialized", () => initializeInput(defaultRecorder.input)));
}
}
/* device list */
{
const currentSelectedDevice = (): SelectedMicrophone => {
let deviceId = defaultRecorder.getDeviceId();
if(deviceId === InputDevice.DefaultDeviceId) {
return { type: "default" };
} else if(deviceId === InputDevice.NoDeviceId) {
return { type: "none" };
} else {
return { type: "device", deviceId: deviceId };
}
};
events.on("query_devices", event => {
if (!getAudioBackend().isInitialized()) {
events.fire_react("notify_devices", {
status: "audio-not-initialized"
});
return;
}
const deviceList = recorderBackend.getDeviceList();
switch (deviceList.getStatus()) {
case "no-permissions":
events.fire_react("notify_devices", {
status: "no-permissions",
shouldAsk: deviceList.getPermissionState() === "denied"
});
return;
case "uninitialized":
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(() => { });
} else {
const devices = deviceList.getDevices();
const defaultDeviceId = getRecorderBackend().getDeviceList().getDefaultDeviceId();
events.fire_react("notify_devices", {
status: "success",
devices: devices.map(e => {
return {
id: e.deviceId,
name: e.name,
driver: e.driver,
default: defaultDeviceId === e.deviceId
}
}),
selectedDevice: currentSelectedDevice(),
});
}
});
events.on("action_set_selected_device", event => {
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;
}
promise.then(() => {
/* TODO:
* This isn't needed since the defaultRecorder might already fire a device change event which will update our ui.
* We only have this since we can't ensure that the recorder does so.
*/
events.fire_react("notify_device_selected", { device: currentSelectedDevice() });
logTrace(LogCategory.GENERAL, tr("Changed default microphone device to %s"), displayName);
}).catch((error) => {
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") });
});
});
events.on("notify_destroy", defaultRecorder.events.on("notify_device_changed", () => {
events.fire_react("notify_device_selected", { device: currentSelectedDevice() });
}));
}
/* settings */
{
events.on("query_setting", event => {
let value;
switch (event.setting) {
case "volume":
value = defaultRecorder.getVolume();
break;
case "threshold-threshold":
value = defaultRecorder.getThresholdThreshold();
break;
case "vad-type":
value = defaultRecorder.getVadType();
break;
case "ppt-key":
value = defaultRecorder.getPushToTalkKey();
break;
case "ppt-release-delay":
value = Math.abs(defaultRecorder.getPushToTalkDelay());
break;
case "ppt-release-delay-active":
value = defaultRecorder.getPushToTalkDelay() > 0;
break;
case "rnnoise":
value = settings.getValue(Settings.KEY_RNNOISE_FILTER);
break;
default:
return;
}
events.fire_react("notify_setting", {setting: event.setting, value: value});
});
events.on("action_set_setting", event => {
const ensure_type = (type: "object" | "string" | "boolean" | "number" | "undefined") => {
if (typeof event.value !== type) {
logWarn(LogCategory.GENERAL, tr("Failed to change microphone setting (Invalid value type supplied. Expected %s, Received: %s)"),
type,
typeof event.value
);
return false;
}
return true;
};
switch (event.setting) {
case "volume":
if (!ensure_type("number")) return;
defaultRecorder.setVolume(event.value);
break;
case "threshold-threshold":
if (!ensure_type("number")) return;
defaultRecorder.setThresholdThreshold(event.value);
break;
case "vad-type":
if (!ensure_type("string")) return;
if (!defaultRecorder.setVadType(event.value)) {
logWarn(LogCategory.GENERAL, tr("Failed to change recorders VAD type to %s"), event.value);
return;
}
break;
case "ppt-key":
if (!ensure_type("object")) return;
defaultRecorder.setPushToTalkKey(event.value);
break;
case "ppt-release-delay":
if (!ensure_type("number")) return;
const sign = defaultRecorder.getPushToTalkDelay() >= 0 ? 1 : -1;
defaultRecorder.setPushToTalkDelay(sign * event.value);
break;
case "ppt-release-delay-active":
if (!ensure_type("boolean")) return;
defaultRecorder.setPushToTalkDelay(Math.abs(defaultRecorder.getPushToTalkDelay()) * (event.value ? 1 : -1));
break;
case "rnnoise":
if (!ensure_type("boolean")) return;
settings.setValue(Settings.KEY_RNNOISE_FILTER, event.value);
break;
default:
return;
}
events.fire_react("notify_setting", {setting: event.setting, value: event.value});
});
}
events.on("action_request_permissions", () => recorderBackend.getDeviceList().requestPermissions().then(result => {
if (result === "granted") {
/* we've nothing to do, the device change event will already update out list */
} else {
events.fire_react("notify_devices", {status: "no-permissions", shouldAsk: result === "denied"});
return;
}
}));
events.on("action_open_processor_properties", () => {
const processor = defaultRecorder.input?.getInputProcessor();
if(!processor) {
createErrorModal(tr("Missing input processor"), tr("Missing default recorders input processor.")).open();
return;
}
spawnInputProcessorModal(processor);
});
events.on("notify_destroy", recorderBackend.getDeviceList().getEvents().on("notify_list_updated", () => {
events.fire("query_devices");
}));
events.on("notify_destroy", recorderBackend.getDeviceList().getEvents().on("notify_state_changed", () => {
events.fire("query_devices");
}));
if(!getAudioBackend().isInitialized()) {
getAudioBackend().executeWhenInitialized(() => events.fire_react("query_devices"));
}
/* TODO: Only do this on user request? */
{
const oldOwner = defaultRecorder.getOwner();
let originalHandlerId = oldOwner instanceof ConnectionRecorderProfileOwner ? oldOwner.getConnection().handlerId : undefined;
ignorePromise(defaultRecorder.ownRecorder(new class extends RecorderProfileOwner {
protected handleRecorderInput(input: AbstractInput) {
input.start().catch(error => {
logError(LogCategory.AUDIO, tr("Failed to start default input: %o"), error);
});
}
protected handleUnmount() {
/* We've been passed to somewhere else */
originalHandlerId = undefined;
}
}));
events.on("notify_destroy", () => {
server_connections.findConnection(originalHandlerId)?.acquireInputHardware().catch(error => {
logError(LogCategory.GENERAL, tr("Failed to acquire microphone after settings detach: %o"), error);
});
});
}
}