mirror of https://github.com/jetkvm/kvm.git
feat: add microphone auto-enable setting with session-based opt-in
Implement session-based microphone control with optional auto-enable: - Microphone disabled by default, only requests permission when enabled - Added persistent "Auto-enable Microphone" setting in audio settings - Setting syncs to backend and works across browsers/devices - Auto-enable respects secure context (HTTPS/localhost only) - Refactored secure context check into reusable utility function - Removed unused audioInputEnabled store properties
This commit is contained in:
parent
d0e6bb1ac6
commit
925b14da1e
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Lydudgang",
|
"audio_settings_output_title": "Lydudgang",
|
||||||
"audio_settings_title": "Lyd",
|
"audio_settings_title": "Lyd",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Udvidelse",
|
||||||
"action_bar_fullscreen": "Fuldskærm",
|
"action_bar_fullscreen": "Fuldskærm",
|
||||||
"action_bar_settings": "Indstillinger",
|
"action_bar_settings": "Indstillinger",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Audioausgang",
|
"audio_settings_output_title": "Audioausgang",
|
||||||
"audio_settings_title": "Audio",
|
"audio_settings_title": "Audio",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Erweiterung",
|
||||||
"action_bar_fullscreen": "Vollbild",
|
"action_bar_fullscreen": "Vollbild",
|
||||||
"action_bar_settings": "Einstellungen",
|
"action_bar_settings": "Einstellungen",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Audio Output",
|
"audio_settings_output_title": "Audio Output",
|
||||||
"audio_settings_title": "Audio",
|
"audio_settings_title": "Audio",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Extension",
|
||||||
"action_bar_fullscreen": "Fullscreen",
|
"action_bar_fullscreen": "Fullscreen",
|
||||||
"action_bar_settings": "Settings",
|
"action_bar_settings": "Settings",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Salida de audio",
|
"audio_settings_output_title": "Salida de audio",
|
||||||
"audio_settings_title": "Audio",
|
"audio_settings_title": "Audio",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Extensión",
|
||||||
"action_bar_fullscreen": "Pantalla completa",
|
"action_bar_fullscreen": "Pantalla completa",
|
||||||
"action_bar_settings": "Ajustes",
|
"action_bar_settings": "Ajustes",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Sortie audio",
|
"audio_settings_output_title": "Sortie audio",
|
||||||
"audio_settings_title": "Audio",
|
"audio_settings_title": "Audio",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Extension",
|
||||||
"action_bar_fullscreen": "Plein écran",
|
"action_bar_fullscreen": "Plein écran",
|
||||||
"action_bar_settings": "Paramètres",
|
"action_bar_settings": "Paramètres",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Uscita audio",
|
"audio_settings_output_title": "Uscita audio",
|
||||||
"audio_settings_title": "Audio",
|
"audio_settings_title": "Audio",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Estensione",
|
||||||
"action_bar_fullscreen": "A schermo intero",
|
"action_bar_fullscreen": "A schermo intero",
|
||||||
"action_bar_settings": "Impostazioni",
|
"action_bar_settings": "Impostazioni",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Lydutgang",
|
"audio_settings_output_title": "Lydutgang",
|
||||||
"audio_settings_title": "Lyd",
|
"audio_settings_title": "Lyd",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Forlengelse",
|
||||||
"action_bar_fullscreen": "Fullskjerm",
|
"action_bar_fullscreen": "Fullskjerm",
|
||||||
"action_bar_settings": "Innstillinger",
|
"action_bar_settings": "Innstillinger",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "Ljudutgång",
|
"audio_settings_output_title": "Ljudutgång",
|
||||||
"audio_settings_title": "Ljud",
|
"audio_settings_title": "Ljud",
|
||||||
"audio_settings_usb_label": "USB",
|
"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_extension": "Förlängning",
|
||||||
"action_bar_fullscreen": "Helskärm",
|
"action_bar_fullscreen": "Helskärm",
|
||||||
"action_bar_settings": "Inställningar",
|
"action_bar_settings": "Inställningar",
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@
|
||||||
"audio_settings_output_title": "音频输出",
|
"audio_settings_output_title": "音频输出",
|
||||||
"audio_settings_title": "音频",
|
"audio_settings_title": "音频",
|
||||||
"audio_settings_usb_label": "USB",
|
"audio_settings_usb_label": "USB",
|
||||||
|
"audio_settings_auto_enable_microphone_title": "自动启用麦克风",
|
||||||
|
"audio_settings_auto_enable_microphone_description": "连接时自动启用浏览器麦克风(否则您必须在每次会话中手动启用)",
|
||||||
"action_bar_extension": "扩展",
|
"action_bar_extension": "扩展",
|
||||||
"action_bar_fullscreen": "全屏",
|
"action_bar_fullscreen": "全屏",
|
||||||
"action_bar_settings": "设置",
|
"action_bar_settings": "设置",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import { keys } from "@/keyboardMappings";
|
import { keys } from "@/keyboardMappings";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { isSecureContext } from "@/utils";
|
||||||
|
|
||||||
export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) {
|
export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) {
|
||||||
// Video and stream related refs and states
|
// Video and stream related refs and states
|
||||||
|
|
@ -33,7 +34,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
|
||||||
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
const [isPointerLockActive, setIsPointerLockActive] = useState(false);
|
||||||
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
const [isKeyboardLockActive, setIsKeyboardLockActive] = useState(false);
|
||||||
|
|
||||||
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
const isPointerLockPossible = isSecureContext();
|
||||||
|
|
||||||
// Store hooks
|
// Store hooks
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import { SettingsItem } from "@components/SettingsItem";
|
import { SettingsItem } from "@components/SettingsItem";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import Checkbox from "@components/Checkbox";
|
import Checkbox from "@components/Checkbox";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { isSecureContext } from "@/utils";
|
||||||
|
|
||||||
export default function AudioPopover() {
|
export default function AudioPopover() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
const { microphoneEnabled, setMicrophoneEnabled } = useSettingsStore();
|
||||||
const [audioOutputEnabled, setAudioOutputEnabled] = useState<boolean>(true);
|
const [audioOutputEnabled, setAudioOutputEnabled] = useState<boolean>(true);
|
||||||
const [audioInputEnabled, setAudioInputEnabled] = useState<boolean>(true);
|
|
||||||
const [usbAudioEnabled, setUsbAudioEnabled] = useState<boolean>(false);
|
const [usbAudioEnabled, setUsbAudioEnabled] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const isHttps = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
const isHttps = isSecureContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
||||||
|
|
@ -25,14 +27,6 @@ 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) => {
|
send("getUsbDevices", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
console.error("Failed to load USB devices:", resp.error);
|
console.error("Failed to load USB devices:", resp.error);
|
||||||
|
|
@ -60,23 +54,6 @@ export default function AudioPopover() {
|
||||||
});
|
});
|
||||||
}, [send]);
|
}, [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 (
|
return (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="space-y-4 p-4 py-3">
|
<div className="space-y-4 p-4 py-3">
|
||||||
|
|
@ -103,7 +80,6 @@ export default function AudioPopover() {
|
||||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
loading={loading}
|
|
||||||
title={m.audio_microphone_title()}
|
title={m.audio_microphone_title()}
|
||||||
description={m.audio_microphone_description()}
|
description={m.audio_microphone_description()}
|
||||||
badge={!isHttps ? m.audio_https_only() : undefined}
|
badge={!isHttps ? m.audio_https_only() : undefined}
|
||||||
|
|
@ -111,9 +87,9 @@ export default function AudioPopover() {
|
||||||
badgeLink={!isHttps ? "settings/access" : undefined}
|
badgeLink={!isHttps ? "settings/access" : undefined}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={audioInputEnabled}
|
checked={microphoneEnabled}
|
||||||
disabled={!isHttps}
|
disabled={!isHttps}
|
||||||
onChange={(e) => handleAudioInputEnabledToggle(e.target.checked)}
|
onChange={(e) => setMicrophoneEnabled(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@ export interface RTCState {
|
||||||
transceiver: RTCRtpTransceiver | null;
|
transceiver: RTCRtpTransceiver | null;
|
||||||
setTransceiver: (transceiver: RTCRtpTransceiver) => void;
|
setTransceiver: (transceiver: RTCRtpTransceiver) => void;
|
||||||
|
|
||||||
|
audioTransceiver: RTCRtpTransceiver | null;
|
||||||
|
setAudioTransceiver: (transceiver: RTCRtpTransceiver) => void;
|
||||||
|
|
||||||
mediaStream: MediaStream | null;
|
mediaStream: MediaStream | null;
|
||||||
setMediaStream: (stream: MediaStream) => void;
|
setMediaStream: (stream: MediaStream) => void;
|
||||||
|
|
||||||
|
|
@ -198,6 +201,9 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
transceiver: null,
|
transceiver: null,
|
||||||
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
||||||
|
|
||||||
|
audioTransceiver: null,
|
||||||
|
setAudioTransceiver: (transceiver: RTCRtpTransceiver) => set({ audioTransceiver: transceiver }),
|
||||||
|
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
|
|
@ -372,8 +378,10 @@ export interface SettingsState {
|
||||||
// Audio settings
|
// Audio settings
|
||||||
audioOutputEnabled: boolean;
|
audioOutputEnabled: boolean;
|
||||||
setAudioOutputEnabled: (enabled: boolean) => void;
|
setAudioOutputEnabled: (enabled: boolean) => void;
|
||||||
audioInputEnabled: boolean;
|
microphoneEnabled: boolean;
|
||||||
setAudioInputEnabled: (enabled: boolean) => void;
|
setMicrophoneEnabled: (enabled: boolean) => void;
|
||||||
|
audioInputAutoEnable: boolean;
|
||||||
|
setAudioInputAutoEnable: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
|
@ -425,8 +433,10 @@ export const useSettingsStore = create(
|
||||||
// Audio settings with defaults
|
// Audio settings with defaults
|
||||||
audioOutputEnabled: true,
|
audioOutputEnabled: true,
|
||||||
setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }),
|
setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }),
|
||||||
audioInputEnabled: true,
|
microphoneEnabled: false,
|
||||||
setAudioInputEnabled: (enabled: boolean) => set({ audioInputEnabled: enabled }),
|
setMicrophoneEnabled: (enabled: boolean) => set({ microphoneEnabled: enabled }),
|
||||||
|
audioInputAutoEnable: false,
|
||||||
|
setAudioInputAutoEnable: (enabled: boolean) => set({ audioInputAutoEnable: enabled }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default function SettingsAudioRoute() {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.setAudioInputEnabled(resp.result as boolean);
|
settings.setAudioInputAutoEnable(resp.result as boolean);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
@ -46,7 +46,7 @@ export default function SettingsAudioRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAudioInputEnabledChange = (enabled: boolean) => {
|
const handleAudioInputAutoEnableChange = (enabled: boolean) => {
|
||||||
send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
send("setAudioInputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
const errorMsg = enabled
|
const errorMsg = enabled
|
||||||
|
|
@ -55,7 +55,7 @@ export default function SettingsAudioRoute() {
|
||||||
notifications.error(errorMsg);
|
notifications.error(errorMsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settings.setAudioInputEnabled(enabled);
|
settings.setAudioInputAutoEnable(enabled);
|
||||||
const successMsg = enabled ? m.audio_input_enabled() : m.audio_input_disabled();
|
const successMsg = enabled ? m.audio_input_enabled() : m.audio_input_disabled();
|
||||||
notifications.success(successMsg);
|
notifications.success(successMsg);
|
||||||
});
|
});
|
||||||
|
|
@ -79,12 +79,12 @@ export default function SettingsAudioRoute() {
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title={m.audio_settings_input_title()}
|
title={m.audio_settings_auto_enable_microphone_title()}
|
||||||
description={m.audio_settings_input_description()}
|
description={m.audio_settings_auto_enable_microphone_description()}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.audioInputEnabled || false}
|
checked={settings.audioInputAutoEnable || false}
|
||||||
onChange={(e) => handleAudioInputEnabledChange(e.target.checked)}
|
onChange={(e) => handleAudioInputAutoEnableChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
useNetworkStateStore,
|
useNetworkStateStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
|
useSettingsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
|
|
@ -51,6 +52,7 @@ import {
|
||||||
} from "@components/VideoOverlay";
|
} from "@components/VideoOverlay";
|
||||||
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { isSecureContext } from "@/utils";
|
||||||
|
|
||||||
export type AuthMode = "password" | "noPassword" | null;
|
export type AuthMode = "password" | "noPassword" | null;
|
||||||
|
|
||||||
|
|
@ -111,6 +113,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
const { sidebarView, setSidebarView, disableVideoFocusTrap, rebootState, setRebootState } = useUiStore();
|
||||||
|
const { microphoneEnabled, setMicrophoneEnabled, audioInputAutoEnable, setAudioInputAutoEnable } = useSettingsStore();
|
||||||
const [queryParams, setQueryParams] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -121,6 +124,8 @@ export default function KvmIdRoute() {
|
||||||
isTurnServerInUse, setTurnServerInUse,
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
|
setAudioTransceiver,
|
||||||
|
audioTransceiver,
|
||||||
setRpcHidChannel,
|
setRpcHidChannel,
|
||||||
setRpcHidUnreliableNonOrderedChannel,
|
setRpcHidUnreliableNonOrderedChannel,
|
||||||
setRpcHidUnreliableChannel,
|
setRpcHidUnreliableChannel,
|
||||||
|
|
@ -530,26 +535,12 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
|
||||||
|
|
||||||
const audioTransceiver = pc.addTransceiver("audio", { direction: "sendrecv" });
|
const audioTrans = pc.addTransceiver("audio", { direction: "sendrecv" });
|
||||||
|
setAudioTransceiver(audioTrans);
|
||||||
|
|
||||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
// Enable microphone if auto-enable is on (only works over HTTPS or localhost)
|
||||||
navigator.mediaDevices.getUserMedia({
|
if (audioInputAutoEnable && isSecureContext()) {
|
||||||
audio: {
|
setMicrophoneEnabled(true);
|
||||||
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");
|
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||||
|
|
@ -603,6 +594,9 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidUnreliableNonOrderedChannel,
|
setRpcHidUnreliableNonOrderedChannel,
|
||||||
setRpcHidUnreliableChannel,
|
setRpcHidUnreliableChannel,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
|
setAudioTransceiver,
|
||||||
|
audioInputAutoEnable,
|
||||||
|
setMicrophoneEnabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -612,6 +606,48 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
}, [peerConnectionState, cleanupAndStopReconnecting]);
|
}, [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
|
// Cleanup effect
|
||||||
const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();
|
const { clearInboundRtpStats, clearCandidatePairStats } = useRTCStore();
|
||||||
|
|
||||||
|
|
@ -770,6 +806,16 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
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(() => {
|
useEffect(() => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
console.log("Requesting video state");
|
console.log("Requesting video state");
|
||||||
|
|
|
||||||
|
|
@ -301,3 +301,7 @@ export function deleteCookie(name: string, domain?: string, path = "/") {
|
||||||
export function sleep(ms: number): Promise<void> {
|
export function sleep(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSecureContext(): boolean {
|
||||||
|
return window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue