[WIP] Improvements, Bugfixes: Improve audio experience when running in HTTP mode

This commit is contained in:
Alex P 2025-09-21 21:04:08 +00:00
parent 093f2bbe22
commit 4dec696c4a
5 changed files with 75 additions and 47 deletions

View File

@ -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

View File

@ -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);
} }
}} }}

View File

@ -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 {

View File

@ -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]);

View File

@ -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(),
}; };
} }