mirror of https://github.com/jetkvm/kvm.git
Compare commits
1 Commits
91d25f2c7f
...
89946c2db8
| Author | SHA1 | Date |
|---|---|---|
|
|
89946c2db8 |
61
audio.go
61
audio.go
|
|
@ -22,6 +22,7 @@ var (
|
||||||
audioLogger zerolog.Logger
|
audioLogger zerolog.Logger
|
||||||
currentAudioTrack *webrtc.TrackLocalStaticSample
|
currentAudioTrack *webrtc.TrackLocalStaticSample
|
||||||
inputTrackHandling atomic.Bool
|
inputTrackHandling atomic.Bool
|
||||||
|
useUSBForAudioOutput atomic.Bool
|
||||||
audioOutputEnabled atomic.Bool
|
audioOutputEnabled atomic.Bool
|
||||||
audioInputEnabled atomic.Bool
|
audioInputEnabled atomic.Bool
|
||||||
)
|
)
|
||||||
|
|
@ -29,10 +30,17 @@ var (
|
||||||
func initAudio() {
|
func initAudio() {
|
||||||
audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger()
|
audioLogger = logging.GetDefaultLogger().With().Str("component", "audio-manager").Logger()
|
||||||
|
|
||||||
|
// Load audio output source from config
|
||||||
|
ensureConfigLoaded()
|
||||||
|
useUSBForAudioOutput.Store(config.AudioOutputSource == "usb")
|
||||||
|
|
||||||
|
// Enable both by default
|
||||||
audioOutputEnabled.Store(true)
|
audioOutputEnabled.Store(true)
|
||||||
audioInputEnabled.Store(true)
|
audioInputEnabled.Store(true)
|
||||||
|
|
||||||
audioLogger.Debug().Msg("Audio subsystem initialized")
|
audioLogger.Debug().
|
||||||
|
Str("source", config.AudioOutputSource).
|
||||||
|
Msg("Audio subsystem initialized")
|
||||||
audioInitialized = true
|
audioInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,8 +56,12 @@ func startAudio() error {
|
||||||
|
|
||||||
// Start output audio if not running and enabled
|
// Start output audio if not running and enabled
|
||||||
if outputSource == nil && audioOutputEnabled.Load() {
|
if outputSource == nil && audioOutputEnabled.Load() {
|
||||||
alsaDevice := "hw:1,0" // USB audio
|
alsaDevice := "hw:0,0" // HDMI
|
||||||
|
if useUSBForAudioOutput.Load() {
|
||||||
|
alsaDevice = "hw:1,0" // USB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CGO audio source
|
||||||
outputSource = audio.NewCgoOutputSource(alsaDevice)
|
outputSource = audio.NewCgoOutputSource(alsaDevice)
|
||||||
|
|
||||||
if currentAudioTrack != nil {
|
if currentAudioTrack != nil {
|
||||||
|
|
@ -150,6 +162,51 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAudioOutputSource switches between HDMI and USB audio output
|
||||||
|
func SetAudioOutputSource(useUSB bool) error {
|
||||||
|
audioMutex.Lock()
|
||||||
|
defer audioMutex.Unlock()
|
||||||
|
|
||||||
|
if useUSBForAudioOutput.Load() == useUSB {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
audioLogger.Info().
|
||||||
|
Bool("old_usb", useUSBForAudioOutput.Load()).
|
||||||
|
Bool("new_usb", useUSB).
|
||||||
|
Msg("Switching audio output source")
|
||||||
|
|
||||||
|
oldValue := useUSBForAudioOutput.Load()
|
||||||
|
useUSBForAudioOutput.Store(useUSB)
|
||||||
|
|
||||||
|
ensureConfigLoaded()
|
||||||
|
if useUSB {
|
||||||
|
config.AudioOutputSource = "usb"
|
||||||
|
} else {
|
||||||
|
config.AudioOutputSource = "hdmi"
|
||||||
|
}
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
audioLogger.Error().Err(err).Msg("Failed to save config")
|
||||||
|
useUSBForAudioOutput.Store(oldValue)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stopOutputLocked()
|
||||||
|
|
||||||
|
// Restart if there are active connections
|
||||||
|
if activeConnections.Load() > 0 {
|
||||||
|
audioMutex.Unlock()
|
||||||
|
err := startAudio()
|
||||||
|
audioMutex.Lock()
|
||||||
|
if err != nil {
|
||||||
|
audioLogger.Error().Err(err).Msg("Failed to restart audio output")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func setPendingInputTrack(track *webrtc.TrackRemote) {
|
func setPendingInputTrack(track *webrtc.TrackRemote) {
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
defer audioMutex.Unlock()
|
defer audioMutex.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ type Config struct {
|
||||||
DefaultLogLevel string `json:"default_log_level"`
|
DefaultLogLevel string `json:"default_log_level"`
|
||||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||||
VideoQualityFactor float64 `json:"video_quality_factor"`
|
VideoQualityFactor float64 `json:"video_quality_factor"`
|
||||||
|
AudioOutputSource string `json:"audio_output_source"` // "hdmi" or "usb"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetDisplayRotation() uint16 {
|
func (c *Config) GetDisplayRotation() uint16 {
|
||||||
|
|
@ -179,6 +180,7 @@ func getDefaultConfig() Config {
|
||||||
return c
|
return c
|
||||||
}(),
|
}(),
|
||||||
DefaultLogLevel: "INFO",
|
DefaultLogLevel: "INFO",
|
||||||
|
AudioOutputSource: "usb",
|
||||||
VideoQualityFactor: 1.0,
|
VideoQualityFactor: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
49
jsonrpc.go
49
jsonrpc.go
|
|
@ -894,9 +894,33 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
|
||||||
func updateUsbRelatedConfig(wasAudioEnabled bool) error {
|
func updateUsbRelatedConfig(wasAudioEnabled bool) error {
|
||||||
ensureConfigLoaded()
|
ensureConfigLoaded()
|
||||||
|
|
||||||
// Stop input audio before USB reconfiguration (input uses USB)
|
audioSourceChanged := false
|
||||||
|
|
||||||
|
// If USB audio is being disabled and audio output source is USB, switch to HDMI
|
||||||
|
if config.UsbDevices != nil && !config.UsbDevices.Audio && config.AudioOutputSource == "usb" {
|
||||||
|
audioMutex.Lock()
|
||||||
|
config.AudioOutputSource = "hdmi"
|
||||||
|
useUSBForAudioOutput.Store(false)
|
||||||
|
audioSourceChanged = true
|
||||||
|
audioMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If USB audio is being enabled (was disabled, now enabled), switch to USB
|
||||||
|
if config.UsbDevices != nil && config.UsbDevices.Audio && !wasAudioEnabled {
|
||||||
|
audioMutex.Lock()
|
||||||
|
config.AudioOutputSource = "usb"
|
||||||
|
useUSBForAudioOutput.Store(true)
|
||||||
|
audioSourceChanged = true
|
||||||
|
audioMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop audio before USB reconfiguration
|
||||||
|
// Input always uses USB, output depends on audioSourceChanged
|
||||||
audioMutex.Lock()
|
audioMutex.Lock()
|
||||||
stopInputLocked()
|
stopInputLocked()
|
||||||
|
if audioSourceChanged {
|
||||||
|
stopOutputLocked()
|
||||||
|
}
|
||||||
audioMutex.Unlock()
|
audioMutex.Unlock()
|
||||||
|
|
||||||
if err := gadget.UpdateGadgetConfig(); err != nil {
|
if err := gadget.UpdateGadgetConfig(); err != nil {
|
||||||
|
|
@ -907,8 +931,9 @@ func updateUsbRelatedConfig(wasAudioEnabled bool) error {
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart audio if USB audio is enabled with active connections
|
// Restart audio if source changed or USB audio is enabled with active connections
|
||||||
if activeConnections.Load() > 0 && config.UsbDevices != nil && config.UsbDevices.Audio {
|
// The relay handles device readiness via retry logic
|
||||||
|
if activeConnections.Load() > 0 && (audioSourceChanged || (config.UsbDevices != nil && config.UsbDevices.Audio)) {
|
||||||
if err := startAudio(); err != nil {
|
if err := startAudio(); err != nil {
|
||||||
logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration")
|
logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration")
|
||||||
}
|
}
|
||||||
|
|
@ -945,6 +970,22 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
|
||||||
return updateUsbRelatedConfig(wasAudioEnabled)
|
return updateUsbRelatedConfig(wasAudioEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetAudioOutputSource() (string, error) {
|
||||||
|
if useUSBForAudioOutput.Load() {
|
||||||
|
return "usb", nil
|
||||||
|
}
|
||||||
|
return "hdmi", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetAudioOutputSource(source string) error {
|
||||||
|
if source != "hdmi" && source != "usb" {
|
||||||
|
return fmt.Errorf("invalid audio output source: %s (must be 'hdmi' or 'usb')", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
useUSB := source == "usb"
|
||||||
|
return SetAudioOutputSource(useUSB)
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetAudioOutputEnabled() (bool, error) {
|
func rpcGetAudioOutputEnabled() (bool, error) {
|
||||||
return audioOutputEnabled.Load(), nil
|
return audioOutputEnabled.Load(), nil
|
||||||
}
|
}
|
||||||
|
|
@ -1279,6 +1320,8 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getUsbDevices": {Func: rpcGetUsbDevices},
|
"getUsbDevices": {Func: rpcGetUsbDevices},
|
||||||
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||||
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||||
|
"getAudioOutputSource": {Func: rpcGetAudioOutputSource},
|
||||||
|
"setAudioOutputSource": {Func: rpcSetAudioOutputSource, Params: []string{"source"}},
|
||||||
"getAudioOutputEnabled": {Func: rpcGetAudioOutputEnabled},
|
"getAudioOutputEnabled": {Func: rpcGetAudioOutputEnabled},
|
||||||
"setAudioOutputEnabled": {Func: rpcSetAudioOutputEnabled, Params: []string{"enabled"}},
|
"setAudioOutputEnabled": {Func: rpcSetAudioOutputEnabled, Params: []string{"enabled"}},
|
||||||
"getAudioInputEnabled": {Func: rpcGetAudioInputEnabled},
|
"getAudioInputEnabled": {Func: rpcGetAudioInputEnabled},
|
||||||
|
|
|
||||||
|
|
@ -370,6 +370,8 @@ export interface SettingsState {
|
||||||
setVideoContrast: (value: number) => void;
|
setVideoContrast: (value: number) => void;
|
||||||
|
|
||||||
// Audio settings
|
// Audio settings
|
||||||
|
audioOutputSource: string;
|
||||||
|
setAudioOutputSource: (source: string) => void;
|
||||||
audioOutputEnabled: boolean;
|
audioOutputEnabled: boolean;
|
||||||
setAudioOutputEnabled: (enabled: boolean) => void;
|
setAudioOutputEnabled: (enabled: boolean) => void;
|
||||||
audioInputEnabled: boolean;
|
audioInputEnabled: boolean;
|
||||||
|
|
@ -423,6 +425,8 @@ export const useSettingsStore = create(
|
||||||
setVideoContrast: (value: number) => set({ videoContrast: value }),
|
setVideoContrast: (value: number) => set({ videoContrast: value }),
|
||||||
|
|
||||||
// Audio settings with defaults
|
// Audio settings with defaults
|
||||||
|
audioOutputSource: "usb",
|
||||||
|
setAudioOutputSource: (source: string) => set({ audioOutputSource: source }),
|
||||||
audioOutputEnabled: true,
|
audioOutputEnabled: true,
|
||||||
setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }),
|
setAudioOutputEnabled: (enabled: boolean) => set({ audioOutputEnabled: enabled }),
|
||||||
audioInputEnabled: true,
|
audioInputEnabled: true,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { SettingsItem } from "@components/SettingsItem";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
// import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
import Checkbox from "@components/Checkbox";
|
import Checkbox from "@components/Checkbox";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
|
|
@ -14,7 +14,15 @@ export default function SettingsAudioRoute() {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
|
// Fetch current audio settings on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
send("getAudioOutputSource", {}, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings.setAudioOutputSource(resp.result as string);
|
||||||
|
});
|
||||||
|
|
||||||
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -31,6 +39,41 @@ export default function SettingsAudioRoute() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
|
const handleAudioOutputSourceChange = (source: string) => {
|
||||||
|
// Update UI immediately for better responsiveness
|
||||||
|
settings.setAudioOutputSource(source);
|
||||||
|
|
||||||
|
send("setAudioOutputSource", { source }, (resp: JsonRpcResponse) => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
// Revert on error by fetching current value from backend
|
||||||
|
send("getAudioOutputSource", {}, (getResp: JsonRpcResponse) => {
|
||||||
|
if ("result" in getResp) {
|
||||||
|
settings.setAudioOutputSource(getResp.result as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
notifications.error(
|
||||||
|
m.audio_settings_output_source_failed({ error: String(resp.error.data || m.unknown_error()) }),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the change was applied by fetching the actual value
|
||||||
|
send("getAudioOutputSource", {}, (getResp: JsonRpcResponse) => {
|
||||||
|
if ("result" in getResp) {
|
||||||
|
const actualSource = getResp.result as string;
|
||||||
|
settings.setAudioOutputSource(actualSource);
|
||||||
|
if (actualSource === source) {
|
||||||
|
notifications.success(m.audio_settings_output_source_success());
|
||||||
|
} else {
|
||||||
|
notifications.error(
|
||||||
|
m.audio_settings_output_source_failed({ error: `Expected ${source}, got ${actualSource}` }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAudioOutputEnabledChange = (enabled: boolean) => {
|
const handleAudioOutputEnabledChange = (enabled: boolean) => {
|
||||||
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
|
@ -78,6 +121,26 @@ export default function SettingsAudioRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
{settings.audioOutputEnabled && (
|
||||||
|
<SettingsItem
|
||||||
|
title={m.audio_settings_output_source_title()}
|
||||||
|
description={m.audio_settings_output_source_description()}
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
value={settings.audioOutputSource || "usb"}
|
||||||
|
options={[
|
||||||
|
{ value: "hdmi", label: m.audio_settings_hdmi_label() },
|
||||||
|
{ value: "usb", label: m.audio_settings_usb_label() },
|
||||||
|
]}
|
||||||
|
onChange={e => {
|
||||||
|
handleAudioOutputSourceChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title={m.audio_settings_input_title()}
|
title={m.audio_settings_input_title()}
|
||||||
description={m.audio_settings_input_description()}
|
description={m.audio_settings_input_description()}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue