fix(stop): Antwort in 0,04 s statt bis 51 s — MP3-Encode in den Hintergrund

POST /api/record/stop wartete auf den kompletten loudnorm-Encode: gemessen
~1,4 s pro aufgenommener Minute (#114: 35,5 min -> 50,9 s). Sichtbar war
davon nichts als ein ausgegrauter Knopf; der Reiterwechsel baute den Knopf
per render() wieder klickbar auf und der zweite Klick lief in
"not recording", weil stopAndSave() current sofort auf null setzt.

- recorder.js: encodeMp3() ohne await, schreibt <name>.mp3.part und
  benennt erst danach um. MP3_ARGS + "-f mp3" (ffmpeg leitet den Muxer aus
  der Endung ab und scheitert an .part).
- web/src/main.js: state.stopping ueberlebt render() -> Knopf bleibt
  gesperrt, Uhr friert ein, Toast mit geschaetzter Wartezeit, zweiter Klick
  verworfen, "not recording" kein roter Fehler. Player laedt nach, solange
  /media 404 liefert (#mp3wait, 5-s-Retry, Abbruch nach 5 min).
- server.js: sweepOrphans erkennt rec_*.mp3.part.

Gegenprobe: 0,033 s / 0,044 s Stop-Antwort, "mp3 fertig" ~2 s spaeter,
64 kbit CBR / 48 kHz / mono ohne Xing-Tabelle, /media 200.

Doku: Kapitel 03 (neuer Abschnitt + Vorher/Nachher-Sequenzdiagramm),
Changelog 2026-08-20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:11:48 +02:00
co-authored by Claude Opus 5
parent bb1711d4c8
commit ec1658ba85
5 changed files with 236 additions and 49 deletions
+4 -1
View File
@@ -632,7 +632,10 @@ async function sweepOrphans() {
const keep = new Set(inUse);
let removed = 0, bytes = 0;
for (const f of fs.readdirSync(REC_DIR)) {
if (!/^rec_.*\.(mp3|wav)$/.test(f) || keep.has(f)) continue;
// `.mp3.part` = ein Encode, der gerade laeuft (recorder.js encodeMp3).
// Faellt der Prozess mitten im Encode aus, bleibt die Halbdatei liegen —
// die Altersgrenze ORPHAN_MIN_AGE_MS raeumt sie dann wie jede Waise weg.
if (!/^rec_.*\.(mp3|wav)(\.part)?$/.test(f) || keep.has(f)) continue;
const p = path.join(REC_DIR, f);
try {
const st = fs.statSync(p);