From 2ff3c8220f86d93b7097d8aadeb5ea7d92ffeb2f Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 20 Apr 2026 21:32:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Mikrofon=20OverconstrainedError=20Fallba?= =?UTF-8?q?ck=20f=C3=BCr=20WebKitGTK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bei OverconstrainedError wird erst Device-Enumeration versucht, dann Fallback auf einfache Constraints. VoicePanel nutzt ebenfalls Fallback von strikten auf einfache Audio-Constraints. Co-Authored-By: Claude Opus 4.6 --- src/lib/components/ChatPanel.svelte | 35 +++++++++++++++++-- src/lib/components/VoicePanel.svelte | 52 +++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index 6165642..086c27b 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -256,7 +256,35 @@ async function startRecording() { try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + let stream: MediaStream; + + try { + stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch (initialErr) { + // OverconstrainedError oder NotFoundError — auf WebKitGTK/Tauri kann + // selbst { audio: true } fehlschlagen. Versuche explizites Device. + if (initialErr instanceof DOMException && + (initialErr.name === 'OverconstrainedError' || initialErr.name === 'NotFoundError')) { + console.warn('getUserMedia fehlgeschlagen, versuche Device-Enumeration:', initialErr); + + const devices = await navigator.mediaDevices.enumerateDevices(); + const audioInput = devices.find(d => d.kind === 'audioinput'); + + if (audioInput) { + console.log('Audio-Device gefunden:', audioInput.label || audioInput.deviceId); + stream = await navigator.mediaDevices.getUserMedia({ + audio: { deviceId: audioInput.deviceId } + }); + } else { + throw new Error( + 'Kein Mikrofon gefunden. Unter WebKitGTK (Tauri/Linux) wird ' + + 'PipeWire oder PulseAudio mit gst-plugin-pipewire benötigt.' + ); + } + } else { + throw initialErr; + } + } // Audio-Analyse für Pegel-Anzeige audioContext = new AudioContext(); @@ -294,7 +322,10 @@ console.log('🎤 Aufnahme gestartet' + (vadEnabled ? ' (VAD aktiv)' : '')); } catch (err) { console.error('Mikrofon-Zugriff fehlgeschlagen:', err); - addMessage('system', `⚠️ Mikrofon-Zugriff fehlgeschlagen: ${err}`); + const hint = (err instanceof DOMException && err.name === 'OverconstrainedError') + ? ' WebKitGTK (Tauri/Linux) unterstützt ggf. kein Audio-Capture — prüfe PipeWire/GStreamer-Plugins.' + : ''; + addMessage('system', `⚠️ Mikrofon-Zugriff fehlgeschlagen: ${err instanceof Error ? err.message : err}${hint}`); } } diff --git a/src/lib/components/VoicePanel.svelte b/src/lib/components/VoicePanel.svelte index 16bccfe..8f9c37d 100644 --- a/src/lib/components/VoicePanel.svelte +++ b/src/lib/components/VoicePanel.svelte @@ -28,6 +28,9 @@ // Transkription (live) let currentTranscript = ''; + // Fehler-Anzeige + let micError = ''; + // TTS Audio-Element let ttsAudio: HTMLAudioElement | null = null; @@ -58,16 +61,27 @@ async function startListening() { if (isListening) return; + micError = ''; try { - // Mikrofon-Zugriff - mediaStream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 16000, - }, - }); + // Mikrofon-Zugriff — zuerst mit optimalen Constraints versuchen, + // bei OverconstrainedError (z.B. WebKitGTK/Tauri) auf Fallback ausweichen + let usedFallback = false; + try { + mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 16000, + }, + }); + console.log('🎤 Mikrofon mit optimalen Constraints geöffnet'); + } catch (constraintErr) { + console.warn('Mikrofon-Constraints fehlgeschlagen, versuche Fallback:', constraintErr); + mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + usedFallback = true; + console.log('🎤 Mikrofon mit Fallback { audio: true } geöffnet'); + } // Audio-Kontext für Visualisierung audioContext = new AudioContext(); @@ -101,9 +115,12 @@ // Visualisierung starten visualize(); - console.log('🎤 Aufnahme gestartet'); + console.log('🎤 Aufnahme gestartet' + (usedFallback ? ' (Fallback-Modus)' : '')); } catch (err) { console.error('Mikrofon-Fehler:', err); + micError = `Mikrofon-Zugriff fehlgeschlagen: ${err instanceof Error ? err.message : err}`; + // Fehler nach 8 Sekunden ausblenden + setTimeout(() => { micError = ''; }, 8000); } } @@ -454,6 +471,13 @@ {/if} + + {#if micError} +
+ ⚠️ {micError} +
+ {/if} +
@@ -691,6 +715,16 @@ font-size: 0.8rem; } + .mic-error { + padding: var(--spacing-sm); + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: var(--radius-md); + color: #ef4444; + font-size: 0.8rem; + margin-bottom: var(--spacing-md); + } + .setup-hint { text-align: center; color: var(--text-secondary);