kvm/ui/src/routes/devices.$id.settings.audio.tsx

312 lines
11 KiB
TypeScript

import { useEffect } from "react";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import Checkbox from "@components/Checkbox";
import { m } from "@localizations/messages.js";
import notifications from "../notifications";
interface AudioConfigResult {
bitrate: number;
complexity: number;
dtx_enabled: boolean;
fec_enabled: boolean;
buffer_periods: number;
packet_loss_perc: number;
}
const AUDIO_DEFAULTS = {
bitrate: 192,
complexity: 8,
packetLossPerc: 20,
} as const;
export default function SettingsAudioRoute() {
const { send } = useJsonRpc();
const handleRpcError = (resp: JsonRpcResponse, defaultMsg?: string) => {
if ("error" in resp) {
notifications.error(String(resp.error.data || defaultMsg || m.unknown_error()));
return true;
}
return false;
};
const {
setAudioOutputEnabled,
setAudioInputAutoEnable,
setAudioOutputSource,
audioOutputEnabled,
audioInputAutoEnable,
audioOutputSource,
audioBitrate,
setAudioBitrate,
audioComplexity,
setAudioComplexity,
audioDTXEnabled,
setAudioDTXEnabled,
audioFECEnabled,
setAudioFECEnabled,
audioBufferPeriods,
setAudioBufferPeriods,
audioPacketLossPerc,
setAudioPacketLossPerc,
} = useSettingsStore();
useEffect(() => {
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
setAudioOutputEnabled(resp.result as boolean);
});
send("getAudioInputAutoEnable", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
setAudioInputAutoEnable(resp.result as boolean);
});
send("getAudioOutputSource", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
setAudioOutputSource(resp.result as string);
});
send("getAudioConfig", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
const config = resp.result as AudioConfigResult;
setAudioBitrate(config.bitrate);
setAudioComplexity(config.complexity);
setAudioDTXEnabled(config.dtx_enabled);
setAudioFECEnabled(config.fec_enabled);
setAudioBufferPeriods(config.buffer_periods);
setAudioPacketLossPerc(config.packet_loss_perc);
});
}, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, setAudioBitrate, setAudioComplexity, setAudioDTXEnabled, setAudioFECEnabled, setAudioBufferPeriods, setAudioPacketLossPerc]);
const handleAudioOutputEnabledChange = (enabled: boolean) => {
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
const errorMsg = enabled
? m.audio_output_failed_enable({ error: String(resp.error.data || m.unknown_error()) })
: m.audio_output_failed_disable({ error: String(resp.error.data || m.unknown_error()) });
notifications.error(errorMsg);
return;
}
setAudioOutputEnabled(enabled);
const successMsg = enabled ? m.audio_output_enabled() : m.audio_output_disabled();
notifications.success(successMsg);
});
};
const handleAudioOutputSourceChange = (source: string) => {
send("setAudioOutputSource", { source }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
const errorMsg = m.audio_settings_output_source_failed({
error: String(resp.error.data || m.unknown_error())
});
notifications.error(errorMsg);
return;
}
setAudioOutputSource(source);
notifications.success(m.audio_settings_output_source_success());
});
};
const handleAudioInputAutoEnableChange = (enabled: boolean) => {
send("setAudioInputAutoEnable", { enabled }, (resp: JsonRpcResponse) => {
if (handleRpcError(resp)) return;
setAudioInputAutoEnable(enabled);
const successMsg = enabled
? m.audio_input_auto_enable_enabled()
: m.audio_input_auto_enable_disabled();
notifications.success(successMsg);
});
};
const getCurrentConfig = () => ({
bitrate: audioBitrate,
complexity: audioComplexity,
dtxEnabled: audioDTXEnabled,
fecEnabled: audioFECEnabled,
bufferPeriods: audioBufferPeriods,
packetLossPerc: audioPacketLossPerc,
});
const handleAudioConfigChange = (updates: Partial<ReturnType<typeof getCurrentConfig>>) => {
const config = { ...getCurrentConfig(), ...updates };
send("setAudioConfig", config, (resp: JsonRpcResponse) => {
if (handleRpcError(resp)) return;
setAudioBitrate(config.bitrate);
setAudioComplexity(config.complexity);
setAudioDTXEnabled(config.dtxEnabled);
setAudioFECEnabled(config.fecEnabled);
setAudioBufferPeriods(config.bufferPeriods);
setAudioPacketLossPerc(config.packetLossPerc);
notifications.success(m.audio_settings_config_updated());
});
};
const handleRestartAudio = () => {
send("restartAudioOutput", {}, (resp: JsonRpcResponse) => {
if (handleRpcError(resp)) return;
notifications.success(m.audio_settings_applied());
});
};
return (
<div className="space-y-4">
<SettingsPageHeader
title={m.audio_settings_title()}
description={m.audio_settings_description()}
/>
<div className="space-y-4">
<SettingsItem
title={m.audio_settings_output_title()}
description={m.audio_settings_output_description()}
>
<Checkbox
checked={audioOutputEnabled || false}
onChange={(e) => handleAudioOutputEnabledChange(e.target.checked)}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_output_source_title()}
description={m.audio_settings_output_source_description()}
>
<SelectMenuBasic
size="SM"
value={audioOutputSource || "usb"}
options={[
{ value: "usb", label: m.audio_settings_usb_label() },
{ value: "hdmi", label: m.audio_settings_hdmi_label() },
]}
onChange={(e) => handleAudioOutputSourceChange(e.target.value)}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_auto_enable_microphone_title()}
description={m.audio_settings_auto_enable_microphone_description()}
>
<Checkbox
checked={audioInputAutoEnable || false}
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_bitrate_title()}
description={m.audio_settings_bitrate_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioBitrate)}
options={[
{ value: "64", label: "64 kbps" },
{ value: "96", label: "96 kbps" },
{ value: "128", label: "128 kbps" },
{ value: "160", label: "160 kbps" },
{ value: "192", label: `192 kbps${192 === AUDIO_DEFAULTS.bitrate ? m.audio_settings_default_suffix() : ''}` },
{ value: "256", label: "256 kbps" },
]}
onChange={(e) => handleAudioConfigChange({ bitrate: parseInt(e.target.value) })}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_complexity_title()}
description={m.audio_settings_complexity_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioComplexity)}
options={[
{ value: "0", label: "0 (fastest)" },
{ value: "2", label: "2" },
{ value: "5", label: "5" },
{ value: "8", label: `8${8 === AUDIO_DEFAULTS.complexity ? m.audio_settings_default_suffix() : ''}` },
{ value: "10", label: "10 (best quality)" },
]}
onChange={(e) => handleAudioConfigChange({ complexity: parseInt(e.target.value) })}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_dtx_title()}
description={m.audio_settings_dtx_description()}
>
<Checkbox
checked={audioDTXEnabled}
onChange={(e) => handleAudioConfigChange({ dtxEnabled: e.target.checked })}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_fec_title()}
description={m.audio_settings_fec_description()}
>
<Checkbox
checked={audioFECEnabled}
onChange={(e) => handleAudioConfigChange({ fecEnabled: e.target.checked })}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_buffer_title()}
description={m.audio_settings_buffer_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioBufferPeriods)}
options={[
{ value: "4", label: "4 (80ms)" },
{ value: "8", label: "8 (160ms)" },
{ value: "12", label: "12 (240ms)" },
{ value: "16", label: "16 (320ms)" },
{ value: "24", label: "24 (480ms)" },
]}
onChange={(e) => handleAudioConfigChange({ bufferPeriods: parseInt(e.target.value) })}
/>
</SettingsItem>
<SettingsItem
title={m.audio_settings_packet_loss_title()}
description={m.audio_settings_packet_loss_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioPacketLossPerc)}
options={[
{ value: "0", label: `0%${m.audio_settings_no_compensation_suffix()}` },
{ value: "5", label: "5%" },
{ value: "10", label: "10%" },
{ value: "15", label: "15%" },
{ value: "20", label: `20%${20 === AUDIO_DEFAULTS.packetLossPerc ? m.audio_settings_default_suffix() : ''}` },
{ value: "25", label: "25%" },
{ value: "30", label: "30%" },
]}
onChange={(e) => handleAudioConfigChange({ packetLossPerc: parseInt(e.target.value) })}
/>
</SettingsItem>
<div className="pt-4">
<button
onClick={handleRestartAudio}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{m.audio_settings_apply_button()}
</button>
</div>
</div>
</div>
);
}