fix(dreams): macOS-aware mic deny message + force-retry escape hatch

The previous "click the lock icon" advice is wrong on macOS, where
"sticky deny" usually originates from the system-level Privacy &
Security setting, not a per-site browser setting. There is no lock
icon to click and the user has no obvious next step.

Now:

- The denied message detects macOS/iOS via navigator.platform and
  walks through the actual fix path: System Settings → Privacy &
  Security → Microphone → enable browser → fully quit and restart it
  (Cmd+Q, not just close the tab — the permission only re-reads on
  cold start). Also points to chrome://settings/content/microphone
  as the second-most-likely culprit
- Non-mac path lists the same two causes in the right order
- Recorder.start now accepts { force: true } that bypasses the
  Permissions API pre-check and actually calls getUserMedia, so the
  raw browser error (NotAllowedError, SecurityError, etc) surfaces.
  Useful when the Permissions API is wrong or stale
- ListView shows a "Trotzdem versuchen" button next to the error
  text. Clicking it routes through the force path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-07 16:10:44 +02:00
parent a6828a16c2
commit f7a5bb841e
2 changed files with 72 additions and 12 deletions

View file

@ -208,6 +208,14 @@
}
}
async function forceRetryMic() {
recError = null;
await dreamRecorder.start({ force: true });
if (dreamRecorder.error) {
recError = dreamRecorder.error;
}
}
function cancelRecording() {
dreamRecorder.cancel();
}
@ -265,7 +273,10 @@
{/if}
</div>
{#if recError}
<p class="rec-error">{recError}</p>
<div class="rec-error">
<p>{recError}</p>
<button class="rec-retry" onclick={forceRetryMic}>Trotzdem versuchen</button>
</div>
{/if}
<!-- Quick create -->
@ -629,10 +640,37 @@
}
.rec-error {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.625rem 0.75rem;
border-radius: 0.375rem;
background: rgba(239, 68, 68, 0.06);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.rec-error p {
font-size: 0.6875rem;
color: #ef4444;
color: #b91c1c;
margin: 0;
padding: 0 0.25rem;
white-space: pre-line;
line-height: 1.5;
}
:global(.dark) .rec-error p {
color: #fca5a5;
}
.rec-retry {
align-self: flex-start;
padding: 0.25rem 0.625rem;
border-radius: 0.25rem;
border: 1px solid rgba(239, 68, 68, 0.3);
background: transparent;
color: #ef4444;
font-size: 0.6875rem;
font-weight: 500;
cursor: pointer;
}
.rec-retry:hover {
background: rgba(239, 68, 68, 0.08);
}
/* ── View Tabs ─────────────────────────────── */

View file

@ -38,7 +38,7 @@ class DreamRecorder {
return typeof window !== 'undefined' && window.isSecureContext === true;
}
async start(): Promise<void> {
async start(options: { force?: boolean } = {}): Promise<void> {
if (this.status !== 'idle') return;
// 1. Secure context check — getUserMedia is silently unavailable
@ -55,14 +55,17 @@ class DreamRecorder {
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;
// 3. Sticky deny check — Permissions API tells us if the browser
// will silently reject getUserMedia without showing a prompt.
// On macOS this is most often a SYSTEM-level block, not a per-site
// setting, which is why no lock icon helps. Skip the check if the
// caller explicitly forces a retry to surface the real error.
if (!options.force) {
const stickyDenied = await this.#checkStickyDeny();
if (stickyDenied) {
this.error = this.#stickyDenyMessage();
return;
}
}
this.error = null;
@ -162,6 +165,25 @@ class DreamRecorder {
reject?.(err);
}
#stickyDenyMessage(): string {
const isMac =
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/i.test(navigator.platform || '');
if (isMac) {
return [
'Mikrofon-Zugriff blockiert. Auf macOS hat das fast immer eine von zwei Ursachen:',
'1) System-Einstellungen → Datenschutz & Sicherheit → Mikrofon: dein Browser muss in der Liste aktiviert sein. Wenn er fehlt oder deaktiviert ist, schalte ihn ein und starte den Browser komplett neu (Cmd+Q, nicht nur Tab schließen).',
'2) Browser-Einstellung: chrome://settings/content/microphone (Chrome) oder about:preferences#privacy (Firefox) → "localhost" darf nicht in der Block-Liste stehen.',
'Tipp: Klicke auf "Trotzdem versuchen" um den exakten Browser-Fehler zu sehen.',
].join('\n');
}
return [
'Mikrofon-Zugriff blockiert. Mögliche Ursachen:',
'1) Browser-Einstellungen → Mikrofon → "localhost" darf nicht blockiert sein.',
'2) System-Einstellungen → Datenschutz → Mikrofon → Browser muss erlaubt sein.',
'Tipp: Klicke auf "Trotzdem versuchen" um den exakten Browser-Fehler zu sehen.',
].join('\n');
}
async #checkStickyDeny(): Promise<boolean> {
try {
// Permissions API may not be available everywhere; treat as unknown.