mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 05:21:10 +02:00
fix(dreams): explain why the mic prompt isn't appearing
The voice capture used to surface a generic "Mikrofon-Zugriff verweigert" whenever getUserMedia rejected, even though the actual cause was usually one of three distinct, fixable conditions: - Insecure context (http://192.168.x.x:5173 instead of localhost or https) — getUserMedia is silently unavailable, no prompt - Sticky deny — user previously refused once, browser remembers and rejects without ever asking again - Hardware: no microphone, busy mic, security policy Now the recorder: 1. Checks window.isSecureContext first and tells the user to switch to https or localhost, naming the offending host 2. Queries the Permissions API for "microphone" before calling getUserMedia. If state is "denied", shows step-by-step recovery instructions (lock icon → mic → allow → reload) instead of pretending the user actively denied just now 3. Maps NotAllowedError / NotFoundError / NotReadableError / SecurityError to specific German messages, with the raw error as a fallback so the rest is still debuggable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
63a6f62529
commit
a6828a16c2
1 changed files with 63 additions and 4 deletions
|
|
@ -34,13 +34,37 @@ class DreamRecorder {
|
|||
);
|
||||
}
|
||||
|
||||
get isSecureContext(): boolean {
|
||||
return typeof window !== 'undefined' && window.isSecureContext === true;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.status !== 'idle') return;
|
||||
|
||||
// 1. Secure context check — getUserMedia is silently unavailable
|
||||
// over plain http (except localhost), with no permission prompt.
|
||||
if (!this.isSecureContext) {
|
||||
const host = typeof window !== 'undefined' ? window.location.host : '';
|
||||
this.error = `Mikrofon-Zugriff braucht eine sichere Verbindung. Öffne die App über https:// oder http://localhost statt http://${host}.`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Browser API present?
|
||||
if (!this.isAvailable) {
|
||||
this.error = 'Audio-Aufnahme wird in diesem Browser nicht unterstützt.';
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Sticky deny check — Permissions API tells us if the user
|
||||
// previously denied access. The browser will silently reject
|
||||
// getUserMedia without showing a prompt in that case.
|
||||
const stickyDenied = await this.#checkStickyDeny();
|
||||
if (stickyDenied) {
|
||||
this.error =
|
||||
'Mikrofon-Zugriff wurde für diese Seite blockiert. Klicke in der Adressleiste auf das Schloss-Symbol → Mikrofon → Erlauben, dann lade die Seite neu.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.status = 'requesting';
|
||||
|
||||
|
|
@ -53,10 +77,7 @@ class DreamRecorder {
|
|||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
this.error = msg.includes('Permission')
|
||||
? 'Mikrofon-Zugriff wurde verweigert.'
|
||||
: `Mikrofon konnte nicht geöffnet werden: ${msg}`;
|
||||
this.error = this.#explainError(e);
|
||||
this.status = 'idle';
|
||||
return;
|
||||
}
|
||||
|
|
@ -141,6 +162,44 @@ class DreamRecorder {
|
|||
reject?.(err);
|
||||
}
|
||||
|
||||
async #checkStickyDeny(): Promise<boolean> {
|
||||
try {
|
||||
// Permissions API may not be available everywhere; treat as unknown.
|
||||
const perms = (
|
||||
navigator as Navigator & {
|
||||
permissions?: {
|
||||
query: (descriptor: { name: string }) => Promise<{ state: string }>;
|
||||
};
|
||||
}
|
||||
).permissions;
|
||||
if (!perms?.query) return false;
|
||||
const status = await perms.query({ name: 'microphone' });
|
||||
return status.state === 'denied';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#explainError(e: unknown): string {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
const name = err.name || '';
|
||||
const msg = err.message || '';
|
||||
|
||||
if (name === 'NotAllowedError' || /denied|permission/i.test(msg)) {
|
||||
return 'Mikrofon-Zugriff wurde verweigert. Klicke in der Adressleiste auf das Schloss-Symbol und erlaube den Zugriff.';
|
||||
}
|
||||
if (name === 'NotFoundError' || /not.?found|no.?device/i.test(msg)) {
|
||||
return 'Kein Mikrofon gefunden. Schließe ein Mikrofon an oder prüfe deine System-Einstellungen.';
|
||||
}
|
||||
if (name === 'NotReadableError' || /in use|busy/i.test(msg)) {
|
||||
return 'Mikrofon ist gerade von einer anderen Anwendung belegt.';
|
||||
}
|
||||
if (name === 'SecurityError') {
|
||||
return 'Mikrofon-Zugriff vom Browser blockiert (Sicherheitsrichtlinie).';
|
||||
}
|
||||
return `Mikrofon konnte nicht geöffnet werden: ${msg || name || 'Unbekannter Fehler'}`;
|
||||
}
|
||||
|
||||
#cleanupStream() {
|
||||
this.#stream?.getTracks().forEach((t) => t.stop());
|
||||
this.#stream = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue