// Aufnahme-Webseite fuer Sprachnotizen an einen Agenten (Issue #85, Folge-Ticket aus #84). // Laeuft im Handy-Browser eines Menschen, getrennt von der Buero-Visualisierung // (office.html/office-render.js) - reines Vanilla-JS/DOM ohne Bundler, wie der Rest des // Frontends. navigator.mediaDevices.getUserMedia()/MediaRecorder verlangen HTTPS (Ausnahme: // localhost) - ohne das bricht init() kontrolliert mit einer Fehlermeldung ab, statt dass der // Start-Knopf stumm nichts tut. import { fetchAgent, uploadRecording } from "./office-api.js"; const agentNameEl = document.getElementById("agent-name"); const statusEl = document.getElementById("status"); const startButton = document.getElementById("start-button"); const stopButton = document.getElementById("stop-button"); const uploadButton = document.getElementById("upload-button"); const discardButton = document.getElementById("discard-button"); const audioPreview = document.getElementById("audio-preview"); const agentId = new URLSearchParams(window.location.search).get("agent"); // MediaRecorder erzeugt je nach Browser unterschiedliche Container/Codecs - der erste vom // Browser unterstuetzte Typ gewinnt. webm/Opus zuerst (Chrome/Firefox/Desktop), mp4/AAC als // Fallback fuer Safari/iOS (kennt kein webm). Muss zur Whitelist in // VoiceRecordingRepository::ALLOWED_MIME_TYPES passen. const CANDIDATE_MIME_TYPES = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg;codecs=opus"]; let mediaRecorder = null; let mediaStream = null; let recordedChunks = []; let recordedBlob = null; function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.toggle("status-error", isError); } function pickMimeType() { if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") { return ""; } return CANDIDATE_MIME_TYPES.find((type) => MediaRecorder.isTypeSupported(type)) || ""; } async function init() { if (!agentId) { setStatus("Kein Agent angegeben - der Link muss ?agent= enthalten.", true); startButton.disabled = true; return; } try { const agent = await fetchAgent(agentId); if (!agent) { setStatus(`Unbekannter Agent (Id ${agentId}).`, true); startButton.disabled = true; return; } agentNameEl.textContent = agent.display_name ?? agent.name ?? `Agent ${agentId}`; } catch { // API evtl. gerade nicht erreichbar - der Upload braucht sie erst am Ende, die Aufnahme // selbst funktioniert auch ohne Namen, deshalb hier nicht hart blockieren. agentNameEl.textContent = `Agent ${agentId}`; } if (!navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== "function") { setStatus("Aufnahme wird von diesem Browser nicht unterstuetzt (HTTPS oder localhost noetig).", true); startButton.disabled = true; } } async function startRecording() { recordedChunks = []; recordedBlob = null; audioPreview.hidden = true; uploadButton.hidden = true; discardButton.hidden = true; try { mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (error) { setStatus(`Mikrofon-Zugriff nicht möglich: ${error.message}`, true); return; } const mimeType = pickMimeType(); mediaRecorder = mimeType ? new MediaRecorder(mediaStream, { mimeType }) : new MediaRecorder(mediaStream); mediaRecorder.addEventListener("dataavailable", (event) => { if (event.data.size > 0) { recordedChunks.push(event.data); } }); mediaRecorder.addEventListener("stop", () => { mediaStream.getTracks().forEach((track) => track.stop()); recordedBlob = new Blob(recordedChunks, { type: mediaRecorder.mimeType || "audio/webm" }); audioPreview.src = URL.createObjectURL(recordedBlob); audioPreview.hidden = false; uploadButton.hidden = false; discardButton.hidden = false; setStatus("Aufnahme beendet - anhören und hochladen oder verwerfen."); }); mediaRecorder.start(); startButton.hidden = true; stopButton.hidden = false; setStatus("Aufnahme läuft …"); } function stopRecording() { if (mediaRecorder && mediaRecorder.state !== "inactive") { mediaRecorder.stop(); } stopButton.hidden = true; startButton.hidden = false; } function discardRecording() { recordedBlob = null; audioPreview.removeAttribute("src"); audioPreview.hidden = true; uploadButton.hidden = true; discardButton.hidden = true; setStatus("Aufnahme verworfen."); } async function upload() { if (!recordedBlob) { return; } uploadButton.disabled = true; setStatus("Wird hochgeladen …"); const extension = (recordedBlob.type.split("/")[1] || "webm").split(";")[0]; try { await uploadRecording(agentId, recordedBlob, `aufnahme.${extension}`); setStatus("Hochgeladen, danke!"); discardRecording(); } catch (error) { setStatus(error.message, true); } finally { uploadButton.disabled = false; } } startButton.addEventListener("click", startRecording); stopButton.addEventListener("click", stopRecording); uploadButton.addEventListener("click", upload); discardButton.addEventListener("click", discardRecording); init();