fix(web): robuster Mikrofon-Pfad fuer iOS-Safari + mp4-Format

- getUserMedia/MediaRecorder-Support pruefen, klare Fehlermeldung mit error.name
  (In-App-Browser/Privat-Modus/Permission) statt pauschalem "HTTPS noetig?"
- Safari liefert audio/mp4 (nicht webm): tatsaechlichen mimeType verwenden und als
  format mitschicken; Server (faster-whisper) erkennt das Format am Inhalt (mp4/AAC
  verifiziert). Asset-Version v7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-18 10:39:02 +02:00
commit d87ebdf457
2 changed files with 26 additions and 8 deletions

View file

@ -201,21 +201,39 @@ let recChunks = [];
async function startRecording() {
if (busy) return;
const md = navigator.mediaDevices;
if (!md || !md.getUserMedia) {
addMessage("system", "Mikrofon nicht freigegeben: Bitte die Seite direkt in Safari öffnen (nicht im In-App-Browser) und keinen Privat-Modus verwenden.");
return;
}
if (typeof MediaRecorder === "undefined") {
addMessage("system", "Aufnahme nicht unterstützt (iOS < 14.3?). Bitte iOS aktualisieren.");
return;
}
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream = await md.getUserMedia({ audio: true });
} catch (e) {
addMessage("system", "Mikrofon nicht verfuegbar (HTTPS noetig?). " + e.message);
addMessage("system", "Mikrofon-Zugriff fehlgeschlagen: " + (e.name || "") +
". Tipp: iPhone → Einstellungen → Safari → Mikrofon = Erlauben/Fragen.");
return;
}
recChunks = [];
mediaRecorder = new MediaRecorder(stream);
try {
mediaRecorder = new MediaRecorder(stream);
} catch (e) {
stream.getTracks().forEach((t) => t.stop());
addMessage("system", "Aufnahme nicht möglich: " + (e.name || e.message));
return;
}
mediaRecorder.ondataavailable = (e) => { if (e.data.size) recChunks.push(e.data); };
mediaRecorder.onstop = async () => {
stream.getTracks().forEach((t) => t.stop());
const blob = new Blob(recChunks, { type: mediaRecorder.mimeType || "audio/webm" });
const mime = mediaRecorder.mimeType || "audio/mp4";
const blob = new Blob(recChunks, { type: mime });
const bytes = await blob.arrayBuffer();
sendVoice(bytes);
// Safari -> audio/mp4, andere -> webm; der Server erkennt das Format am Inhalt.
sendVoice(bytes, mime.includes("mp4") || mime.includes("mpeg") ? "mp4" : "webm");
};
mediaRecorder.start();
micBtn.classList.remove("bg-emerald-600");
@ -229,13 +247,13 @@ function stopRecording() {
micBtn.classList.add("bg-emerald-600");
}
async function sendVoice(bytes) {
async function sendVoice(bytes, fmt = "webm") {
if (busy) return;
busy = true;
await ensureSession();
statusEl.textContent = "verarbeite Sprache …";
await runTurn("/ws/voice", (ws) => {
ws.send(JSON.stringify({ type: "start", format: "webm", stream: true, audio_stream: true, ...overrides() }));
ws.send(JSON.stringify({ type: "start", format: fmt, stream: true, audio_stream: true, ...overrides() }));
ws.send(bytes);
ws.send(JSON.stringify({ type: "end" }));
});

View file

@ -60,6 +60,6 @@
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
</footer>
<script src="/app.js?v=6"></script>
<script src="/app.js?v=7"></script>
</body>
</html>