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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
previousAudioEnabled := config.UsbDevices != nil && config.UsbDevices.Audio
|
||||
newAudioEnabled := usbDevices.Audio
|
||||
|
@ -1034,11 +1003,6 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
|
|||
case "massStorage":
|
||||
config.UsbDevices.MassStorage = enabled
|
||||
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
|
||||
if !enabled {
|
||||
// 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
|
||||
|
||||
// Get USB device configuration to check if audio is enabled
|
||||
const { usbDeviceConfig } = useUsbDeviceConfig();
|
||||
const isAudioEnabledInUsb = usbDeviceConfig?.audio ?? true; // Default to true while loading
|
||||
const { usbDeviceConfig, loading: usbConfigLoading } = useUsbDeviceConfig();
|
||||
// Default to false while loading to prevent premature access when audio hasn't been enabled yet
|
||||
const isAudioEnabledInUsb = usbDeviceConfig?.audio ?? false;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Popover>
|
||||
<PopoverButton as={Fragment} disabled={!isAudioEnabledInUsb}>
|
||||
<div title={!isAudioEnabledInUsb ? "Audio needs to be enabled in USB device settings" : undefined}>
|
||||
<PopoverButton as={Fragment} disabled={!isAudioEnabledInUsb || usbConfigLoading}>
|
||||
<div title={
|
||||
usbConfigLoading
|
||||
? "Loading audio configuration..."
|
||||
: !isAudioEnabledInUsb
|
||||
? "Audio needs to be enabled in USB device settings"
|
||||
: undefined
|
||||
}>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
text="Audio"
|
||||
disabled={!isAudioEnabledInUsb}
|
||||
disabled={!isAudioEnabledInUsb || usbConfigLoading}
|
||||
LeadingIcon={({ className }) => (
|
||||
<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")} />
|
||||
) : isMuted ? (
|
||||
<MdVolumeOff className={cx(className, "text-red-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>
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isAudioEnabledInUsb) {
|
||||
if (isAudioEnabledInUsb && !usbConfigLoading) {
|
||||
setDisableVideoFocusTrap(true);
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -29,6 +29,8 @@ interface MicrophoneHookReturn {
|
|||
isStarting: boolean;
|
||||
isStopping: boolean;
|
||||
isToggling: boolean;
|
||||
// HTTP/HTTPS detection
|
||||
isHttpsRequired: boolean;
|
||||
}
|
||||
|
||||
interface AudioConfig {
|
||||
|
|
|
@ -33,6 +33,11 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
|||
setError(null);
|
||||
|
||||
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
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
|
@ -69,7 +74,23 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
|||
|
||||
} catch (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 {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
@ -82,13 +103,19 @@ export function useAudioDevices(): UseAudioDevicesReturn {
|
|||
refreshDevices();
|
||||
};
|
||||
|
||||
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
|
||||
// Check if navigator.mediaDevices exists and supports addEventListener
|
||||
if (navigator.mediaDevices && typeof navigator.mediaDevices.addEventListener === 'function') {
|
||||
navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
|
||||
}
|
||||
|
||||
// Initial load
|
||||
refreshDevices();
|
||||
|
||||
return () => {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange);
|
||||
// Check if navigator.mediaDevices exists and supports removeEventListener
|
||||
if (navigator.mediaDevices && typeof navigator.mediaDevices.removeEventListener === 'function') {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', handleDeviceChange);
|
||||
}
|
||||
};
|
||||
}, [refreshDevices]);
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ export interface MicrophoneError {
|
|||
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() {
|
||||
const {
|
||||
peerConnection,
|
||||
|
@ -187,6 +192,20 @@ export function useMicrophone() {
|
|||
try {
|
||||
// Set flag to prevent sync during startup
|
||||
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
|
||||
const audioConstraints: MediaTrackConstraints = {
|
||||
echoCancellation: true,
|
||||
|
@ -666,5 +685,8 @@ export function useMicrophone() {
|
|||
isStarting,
|
||||
isStopping,
|
||||
isToggling,
|
||||
|
||||
// HTTP/HTTPS detection
|
||||
isHttpsRequired: isHttpsRequired(),
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue