mirror of https://github.com/jetkvm/kvm.git
[WIP] Improvements, Bugfixes: Improve audio experience when running in HTTP mode
This commit is contained in:
parent
093f2bbe22
commit
4dec696c4a
36
jsonrpc.go
36
jsonrpc.go
|
@ -908,38 +908,7 @@ func updateUsbRelatedConfig() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAudioConfiguration checks if audio functionality can be enabled
|
|
||||||
func validateAudioConfiguration(enabled bool) error {
|
|
||||||
if !enabled {
|
|
||||||
return nil // Disabling audio is always allowed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if audio supervisor is available
|
|
||||||
if audioSupervisor == nil {
|
|
||||||
return fmt.Errorf("audio supervisor not initialized - audio functionality not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if ALSA devices are available by attempting to list them
|
|
||||||
// This is a basic check to ensure the system has audio capabilities
|
|
||||||
if _, err := os.Stat("/proc/asound/cards"); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("no ALSA sound cards detected - audio hardware not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if USB gadget audio function is supported
|
|
||||||
if _, err := os.Stat("/sys/kernel/config/usb_gadget"); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("USB gadget configfs not available - cannot enable USB audio")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
|
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
|
||||||
// Validate audio configuration before proceeding
|
|
||||||
if err := validateAudioConfiguration(usbDevices.Audio); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("audio configuration validation failed")
|
|
||||||
return fmt.Errorf("audio validation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if audio state is changing
|
// Check if audio state is changing
|
||||||
previousAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
|
previousAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
|
||||||
newAudioEnabled := usbDevices.Audio
|
newAudioEnabled := usbDevices.Audio
|
||||||
|
@ -1034,11 +1003,6 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
|
||||||
case "massStorage":
|
case "massStorage":
|
||||||
config.UsbDevices.MassStorage = enabled
|
config.UsbDevices.MassStorage = enabled
|
||||||
case "audio":
|
case "audio":
|
||||||
// Validate audio configuration before proceeding
|
|
||||||
if err := validateAudioConfiguration(enabled); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("audio device state validation failed")
|
|
||||||
return fmt.Errorf("audio validation failed: %w", err)
|
|
||||||
}
|
|
||||||
// Handle audio process management
|
// Handle audio process management
|
||||||
if !enabled {
|
if !enabled {
|
||||||
// Stop audio processes when audio is disabled
|
// Stop audio processes when audio is disabled
|
||||||
|
|
|
@ -86,8 +86,9 @@ export default function Actionbar({
|
||||||
const isMuted = audioMuted ?? false; // Default to false if WebSocket data not available yet
|
const isMuted = audioMuted ?? false; // Default to false if WebSocket data not available yet
|
||||||
|
|
||||||
// Get USB device configuration to check if audio is enabled
|
// Get USB device configuration to check if audio is enabled
|
||||||
const { usbDeviceConfig } = useUsbDeviceConfig();
|
const { usbDeviceConfig, loading: usbConfigLoading } = useUsbDeviceConfig();
|
||||||
const isAudioEnabledInUsb = usbDeviceConfig?.audio ?? true; // Default to true while loading
|
// Default to false while loading to prevent premature access when audio hasn't been enabled yet
|
||||||
|
const isAudioEnabledInUsb = usbDeviceConfig?.audio ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
|
||||||
|
@ -320,27 +321,39 @@ export default function Actionbar({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverButton as={Fragment} disabled={!isAudioEnabledInUsb}>
|
<PopoverButton as={Fragment} disabled={!isAudioEnabledInUsb || usbConfigLoading}>
|
||||||
<div title={!isAudioEnabledInUsb ? "Audio needs to be enabled in USB device settings" : undefined}>
|
<div title={
|
||||||
|
usbConfigLoading
|
||||||
|
? "Loading audio configuration..."
|
||||||
|
: !isAudioEnabledInUsb
|
||||||
|
? "Audio needs to be enabled in USB device settings"
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Audio"
|
text="Audio"
|
||||||
disabled={!isAudioEnabledInUsb}
|
disabled={!isAudioEnabledInUsb || usbConfigLoading}
|
||||||
LeadingIcon={({ className }) => (
|
LeadingIcon={({ className }) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!isAudioEnabledInUsb ? (
|
{usbConfigLoading ? (
|
||||||
|
<div className={cx(className, "animate-spin rounded-full border border-gray-400 border-t-gray-600")} />
|
||||||
|
) : !isAudioEnabledInUsb ? (
|
||||||
<MdVolumeOff className={cx(className, "text-gray-400")} />
|
<MdVolumeOff className={cx(className, "text-gray-400")} />
|
||||||
) : isMuted ? (
|
) : isMuted ? (
|
||||||
<MdVolumeOff className={cx(className, "text-red-500")} />
|
<MdVolumeOff className={cx(className, "text-red-500")} />
|
||||||
) : (
|
) : (
|
||||||
<MdVolumeUp className={cx(className, "text-green-500")} />
|
<MdVolumeUp className={cx(className, "text-green-500")} />
|
||||||
)}
|
)}
|
||||||
<MdGraphicEq className={cx(className, "ml-1", !isAudioEnabledInUsb ? "text-gray-400" : "text-blue-500")} />
|
<MdGraphicEq className={cx(className, "ml-1",
|
||||||
|
usbConfigLoading ? "text-gray-400" :
|
||||||
|
!isAudioEnabledInUsb ? "text-gray-400" :
|
||||||
|
"text-blue-500"
|
||||||
|
)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAudioEnabledInUsb) {
|
if (isAudioEnabledInUsb && !usbConfigLoading) {
|
||||||
setDisableVideoFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -29,6 +29,8 @@ interface MicrophoneHookReturn {
|
||||||
isStarting: boolean;
|
isStarting: boolean;
|
||||||
isStopping: boolean;
|
isStopping: boolean;
|
||||||
isToggling: boolean;
|
isToggling: boolean;
|
||||||
|
// HTTP/HTTPS detection
|
||||||
|
isHttpsRequired: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AudioConfig {
|
interface AudioConfig {
|
||||||
|
|
|
@ -33,6 +33,11 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if getUserMedia is available (requires HTTPS in most browsers)
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error('Microphone access requires HTTPS connection. Please use HTTPS to access audio features.');
|
||||||
|
}
|
||||||
|
|
||||||
// Request permissions first to get device labels
|
// Request permissions first to get device labels
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
@ -69,7 +74,23 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
devError('Failed to enumerate audio devices:', err);
|
devError('Failed to enumerate audio devices:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to access audio devices');
|
let errorMessage = 'Failed to access audio devices';
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.message.includes('HTTPS')) {
|
||||||
|
errorMessage = err.message;
|
||||||
|
} else if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
||||||
|
errorMessage = 'Microphone permission denied. Please allow microphone access.';
|
||||||
|
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
||||||
|
errorMessage = 'No microphone devices found.';
|
||||||
|
} else if (err.name === 'NotSupportedError') {
|
||||||
|
errorMessage = 'Audio devices are not supported on this connection. Please use HTTPS.';
|
||||||
|
} else {
|
||||||
|
errorMessage = err.message || errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -82,13 +103,19 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if navigator.mediaDevices exists and supports addEventListener
|
||||||
|
if (navigator.mediaDevices && typeof navigator.mediaDevices.addEventListener === 'function') {
|
||||||
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
|
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
|
||||||
|
}
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
// Check if navigator.mediaDevices exists and supports removeEventListener
|
||||||
|
if (navigator.mediaDevices && typeof navigator.mediaDevices.removeEventListener === 'function') {
|
||||||
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange);
|
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [refreshDevices]);
|
}, [refreshDevices]);
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,11 @@ export interface MicrophoneError {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check if HTTPS is required for microphone access
|
||||||
|
export function isHttpsRequired(): boolean {
|
||||||
|
return !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia;
|
||||||
|
}
|
||||||
|
|
||||||
export function useMicrophone() {
|
export function useMicrophone() {
|
||||||
const {
|
const {
|
||||||
peerConnection,
|
peerConnection,
|
||||||
|
@ -187,6 +192,20 @@ export function useMicrophone() {
|
||||||
try {
|
try {
|
||||||
// Set flag to prevent sync during startup
|
// Set flag to prevent sync during startup
|
||||||
isStartingRef.current = true;
|
isStartingRef.current = true;
|
||||||
|
|
||||||
|
// Check if getUserMedia is available (requires HTTPS in most browsers)
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
setIsStarting(false);
|
||||||
|
isStartingRef.current = false;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'permission',
|
||||||
|
message: 'Microphone access requires HTTPS connection. Please use HTTPS to use audio input.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Request microphone permission and get stream
|
// Request microphone permission and get stream
|
||||||
const audioConstraints: MediaTrackConstraints = {
|
const audioConstraints: MediaTrackConstraints = {
|
||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
|
@ -666,5 +685,8 @@ export function useMicrophone() {
|
||||||
isStarting,
|
isStarting,
|
||||||
isStopping,
|
isStopping,
|
||||||
isToggling,
|
isToggling,
|
||||||
|
|
||||||
|
// HTTP/HTTPS detection
|
||||||
|
isHttpsRequired: isHttpsRequired(),
|
||||||
};
|
};
|
||||||
}
|
}
|
Loading…
Reference in New Issue