Compare commits

..

1 Commits

Author SHA1 Message Date
Alex 396b0165b3
Merge d0e6bb1ac6 into 1d1e58f036 2025-11-03 13:56:08 +01:00
15 changed files with 61 additions and 116 deletions

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Lydudgang",
"audio_settings_title": "Lyd",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk",
"audio_settings_auto_enable_microphone_description": "Aktiver automatisk browsermikrofon ved tilslutning (ellers skal du aktivere det manuelt ved hver session)",
"action_bar_extension": "Udvidelse",
"action_bar_fullscreen": "Fuldskærm",
"action_bar_settings": "Indstillinger",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Audioausgang",
"audio_settings_title": "Audio",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Mikrofon automatisch aktivieren",
"audio_settings_auto_enable_microphone_description": "Browser-Mikrofon beim Verbinden automatisch aktivieren (andernfalls müssen Sie es in jeder Sitzung manuell aktivieren)",
"action_bar_extension": "Erweiterung",
"action_bar_fullscreen": "Vollbild",
"action_bar_settings": "Einstellungen",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Audio Output",
"audio_settings_title": "Audio",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Auto-enable Microphone",
"audio_settings_auto_enable_microphone_description": "Automatically enable browser microphone when connecting (otherwise you must manually enable each session)",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Fullscreen",
"action_bar_settings": "Settings",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Salida de audio",
"audio_settings_title": "Audio",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Habilitar micrófono automáticamente",
"audio_settings_auto_enable_microphone_description": "Habilitar automáticamente el micrófono del navegador al conectar (de lo contrario, debe habilitarlo manualmente en cada sesión)",
"action_bar_extension": "Extensión",
"action_bar_fullscreen": "Pantalla completa",
"action_bar_settings": "Ajustes",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Sortie audio",
"audio_settings_title": "Audio",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Activer automatiquement le microphone",
"audio_settings_auto_enable_microphone_description": "Activer automatiquement le microphone du navigateur lors de la connexion (sinon vous devez l'activer manuellement à chaque session)",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Plein écran",
"action_bar_settings": "Paramètres",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Uscita audio",
"audio_settings_title": "Audio",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Abilita automaticamente il microfono",
"audio_settings_auto_enable_microphone_description": "Abilita automaticamente il microfono del browser durante la connessione (altrimenti devi abilitarlo manualmente ad ogni sessione)",
"action_bar_extension": "Estensione",
"action_bar_fullscreen": "A schermo intero",
"action_bar_settings": "Impostazioni",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Lydutgang",
"audio_settings_title": "Lyd",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Aktiver mikrofon automatisk",
"audio_settings_auto_enable_microphone_description": "Aktiver automatisk nettlesermikrofon ved tilkobling (ellers må du aktivere det manuelt hver økt)",
"action_bar_extension": "Forlengelse",
"action_bar_fullscreen": "Fullskjerm",
"action_bar_settings": "Innstillinger",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "Ljudutgång",
"audio_settings_title": "Ljud",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "Aktivera mikrofon automatiskt",
"audio_settings_auto_enable_microphone_description": "Aktivera automatiskt webbläsarmikrofon vid anslutning (annars måste du aktivera den manuellt varje session)",
"action_bar_extension": "Förlängning",
"action_bar_fullscreen": "Helskärm",
"action_bar_settings": "Inställningar",

View File

@ -82,8 +82,6 @@
"audio_settings_output_title": "音频输出",
"audio_settings_title": "音频",
"audio_settings_usb_label": "USB",
"audio_settings_auto_enable_microphone_title": "自动启用麦克风",
"audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)",
"action_bar_extension": "扩展",
"action_bar_fullscreen": "全屏",
"action_bar_settings": "设置",

View File

@ -22,7 +22,6 @@ import {
import { keys } from "@/keyboardMappings";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
import { isSecureContext } from "@/utils";
export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) {
// Video and stream related refs and states
@ -34,7 +33,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
const isPointerLockPossible = isSecureContext();
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
// Store hooks
const settings = useSettingsStore();

View File

@ -1,22 +1,20 @@
import { useCallback, useEffect, useState } from "react";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useSettingsStore } from "@/hooks/stores";
import { GridCard } from "@components/Card";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import Checkbox from "@components/Checkbox";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
import { isSecureContext } from "@/utils";
export default function AudioPopover() {
const { send } = useJsonRpc();
const { microphoneEnabled, setMicrophoneEnabled } = useSettingsStore();
const [audioOutputEnabled, setAudioOutputEnabled] = useState<boolean>(true);
const [audioInputEnabled, setAudioInputEnabled] = useState<boolean>(true);
const [usbAudioEnabled, setUsbAudioEnabled] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
const isHttps = isSecureContext();
const isHttps = window.location.protocol === "https:" || window.location.hostname === "localhost";
useEffect(() => {
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
@ -27,6 +25,14 @@ export default function AudioPopover() {
}
});
send("getAudioInputEnabled", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to load audio input enabled:", resp.error);
} else {
setAudioInputEnabled(resp.result as boolean);
}
});
send("getUsbDevices", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to load USB devices:", resp.error);
@ -54,6 +60,23 @@ export default function AudioPopover() {
});
}, [send]);
const handleAudioInputEnabledToggle = useCallback((enabled: boolean) => {
setLoading(true);
send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => {
setLoading(false);
if ("error" in resp) {
const errorMsg = enabled
? m.audio_input_failed_enable({ error: String(resp.error.data || m.unknown_error()) })
: m.audio_input_failed_disable({ error: String(resp.error.data || m.unknown_error()) });
notifications.error(errorMsg);
} else {
setAudioInputEnabled(enabled);
const successMsg = enabled ? m.audio_input_enabled() : m.audio_input_disabled();
notifications.success(successMsg);
}
});
}, [send]);
return (
<GridCard>
<div className="space-y-4 p-4 py-3">
@ -80,6 +103,7 @@ export default function AudioPopover() {
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsItem
loading={loading}
title={m.audio_microphone_title()}
description={m.audio_microphone_description()}
badge={!isHttps ? m.audio_https_only() : undefined}
@ -87,9 +111,9 @@ export default function AudioPopover() {
badgeLink={!isHttps ? "settings/access" : undefined}
>
<Checkbox
checked={microphoneEnabled}
checked={audioInputEnabled}
disabled={!isHttps}
onChange={(e) => setMicrophoneEnabled(e.target.checked)}
onChange={(e) => handleAudioInputEnabledToggle(e.target.checked)}
/>
</SettingsItem>
</>

View File

@ -140,9 +140,6 @@ export interface RTCState {
transceiver: RTCRtpTransceiver | null;
setTransceiver: (transceiver: RTCRtpTransceiver) => void;
audioTransceiver: RTCRtpTransceiver | null;
setAudioTransceiver: (transceiver: RTCRtpTransceiver) => void;
mediaStream: MediaStream | null;
setMediaStream: (stream: MediaStream) => void;
@ -201,9 +198,6 @@ export const useRTCStore = create<RTCState>(set => ({
transceiver: null,
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
audioTransceiver: null,
setAudioTransceiver: (transceiver: RTCRtpTransceiver) => set({ audioTransceiver: transceiver }),
peerConnectionState: null,
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
@ -378,10 +372,8 @@ export interface SettingsState {
// Audio settings
audioOutputEnabled: boolean;
setAudioOutputEnabled: (enabled: boolean) => void;
microphoneEnabled: boolean;
setMicrophoneEnabled: (enabled: boolean) => void;
audioInputAutoEnable: boolean;
setAudioInputAutoEnable: (enabled: boolean) => void;
audioInputEnabled: boolean;
setAudioInputEnabled: (enabled: boolean) => void;
}
export const useSettingsStore = create(
@ -433,10 +425,8 @@ export const useSettingsStore = create(
// Audio settings with defaults
audioOutputEnabled: true,
setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }),
microphoneEnabled: false,
setMicrophoneEnabled: (enabled: boolean) => set({ microphoneEnabled: enabled }),
audioInputAutoEnable: false,
setAudioInputAutoEnable: (enabled: boolean) => set({ audioInputAutoEnable: enabled }),
audioInputEnabled: true,
setAudioInputEnabled: (enabled: boolean) => set({ audioInputEnabled: enabled }),
}),
{
name: "settings",

View File

@ -26,7 +26,7 @@ export default function SettingsAudioRoute() {
if ("error" in resp) {
return;
}
settings.setAudioInputAutoEnable(resp.result as boolean);
settings.setAudioInputEnabled(resp.result as boolean);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [send]);
@ -46,7 +46,7 @@ export default function SettingsAudioRoute() {
});
};
const handleAudioInputAutoEnableChange = (enabled: boolean) => {
const handleAudioInputEnabledChange = (enabled: boolean) => {
send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
const errorMsg = enabled
@ -55,7 +55,7 @@ export default function SettingsAudioRoute() {
notifications.error(errorMsg);
return;
}
settings.setAudioInputAutoEnable(enabled);
settings.setAudioInputEnabled(enabled);
const successMsg = enabled ? m.audio_input_enabled() : m.audio_input_disabled();
notifications.success(successMsg);
});
@ -79,12 +79,12 @@ export default function SettingsAudioRoute() {
</SettingsItem>
<SettingsItem
title={m.audio_settings_auto_enable_microphone_title()}
description={m.audio_settings_auto_enable_microphone_description()}
title={m.audio_settings_input_title()}
description={m.audio_settings_input_description()}
>
<Checkbox
checked={settings.audioInputAutoEnable || false}
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
checked={settings.audioInputEnabled || false}
onChange={(e) => handleAudioInputEnabledChange(e.target.checked)}
/>
</SettingsItem>
</div>

View File

@ -29,7 +29,6 @@ import {
useNetworkStateStore,
User,
useRTCStore,
useSettingsStore,
useUiStore,
useUpdateStore,
useVideoStore,
@ -52,7 +51,6 @@ import {
} from "@components/VideoOverlay";
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
import { m } from "@localizations/messages.js";
import { isSecureContext } from "@/utils";
export type AuthMode = "password" | "noPassword" | null;
@ -113,7 +111,6 @@ export default function KvmIdRoute() {
const params = useParams() as { id: string };
const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
const { microphoneEnabled, setMicrophoneEnabled, audioInputAutoEnable, setAudioInputAutoEnable } = useSettingsStore();
const [queryParams, setQueryParams] = useSearchParams();
const {
@ -124,8 +121,6 @@ export default function KvmIdRoute() {
isTurnServerInUse, setTurnServerInUse,
rpcDataChannel,
setTransceiver,
setAudioTransceiver,
audioTransceiver,
setRpcHidChannel,
setRpcHidUnreliableNonOrderedChannel,
setRpcHidUnreliableChannel,
@ -535,12 +530,26 @@ export default function KvmIdRoute() {
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
const audioTrans = pc.addTransceiver("audio", { direction: "sendrecv" });
setAudioTransceiver(audioTrans);
const audioTransceiver = pc.addTransceiver("audio", { direction: "sendrecv" });
// Enable microphone if auto-enable is on (only works over HTTPS or localhost)
if (audioInputAutoEnable && isSecureContext()) {
setMicrophoneEnabled(true);
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
channelCount: 2, // Request stereo input if available
}
}).then((stream) => {
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack && audioTransceiver.sender) {
audioTransceiver.sender.replaceTrack(audioTrack);
}
}).catch((err) => {
console.warn("Microphone access denied or unavailable:", err.message);
});
} else {
console.warn("navigator.mediaDevices.getUserMedia is not available in this browser/context");
}
const rpcDataChannel = pc.createDataChannel("rpc");
@ -594,9 +603,6 @@ export default function KvmIdRoute() {
setRpcHidUnreliableNonOrderedChannel,
setRpcHidUnreliableChannel,
setTransceiver,
setAudioTransceiver,
audioInputAutoEnable,
setMicrophoneEnabled,
]);
useEffect(() => {
@ -606,48 +612,6 @@ export default function KvmIdRoute() {
}
}, [peerConnectionState, cleanupAndStopReconnecting]);
// Handle dynamic microphone enable/disable
useEffect(() => {
if (!audioTransceiver || !peerConnection) return;
if (microphoneEnabled) {
// Request microphone access
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
channelCount: 2,
}
}).then((stream) => {
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack && audioTransceiver.sender) {
audioTransceiver.sender.replaceTrack(audioTrack);
console.log("Microphone enabled");
}
}).catch((err) => {
console.warn("Microphone access denied or unavailable:", err.message);
setMicrophoneEnabled(false);
});
}
} else {
// Disable microphone by removing the track
if (audioTransceiver.sender.track) {
audioTransceiver.sender.track.stop();
audioTransceiver.sender.replaceTrack(null);
console.log("Microphone disabled");
}
}
}, [microphoneEnabled, audioTransceiver, peerConnection, setMicrophoneEnabled]);
// Auto-enable microphone when setting is loaded from backend
useEffect(() => {
if (audioInputAutoEnable && audioTransceiver && peerConnection && !microphoneEnabled && isSecureContext()) {
setMicrophoneEnabled(true);
}
}, [audioInputAutoEnable, audioTransceiver, peerConnection, microphoneEnabled, setMicrophoneEnabled]);
// Cleanup effect
const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();
@ -806,16 +770,6 @@ export default function KvmIdRoute() {
const { send } = useJsonRpc(onJsonRpcRequest);
// Load audio input auto-enable setting from backend on mount
useEffect(() => {
send("getAudioInputEnabled", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
return;
}
setAudioInputAutoEnable(resp.result as boolean);
});
}, [send, setAudioInputAutoEnable]);
useEffect(() => {
if (rpcDataChannel?.readyState !== "open") return;
console.log("Requesting video state");

View File

@ -301,7 +301,3 @@ export function deleteCookie(name: string, domain?: string, path = "/") {
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function isSecureContext(): boolean {
return window.location.protocol === "https:" || window.location.hostname === "localhost";
}