Files
dogfather-universe/server/helfer-ffmpeg.mjs
T
DogFatherGitandClaude Opus 5 d70d00cedc Jede Sprachnachricht in einer Fassung, die JEDES Geraet abspielt
Miss im Support, Meldung #12 -- fuenf Runden seit dem 23.09.: „Bei
mir laesst sich die Nachricht nicht abspielen." Zuletzt: „Jetzt laedt
es die ganze Zeit." Bei allen anderen ging es.

GEMESSEN, BEVOR GEBAUT WURDE

  · Alle sechs Sprachnachrichten im Haus sind audio/webm (Opus).
  · Miss' Geraet: iPhone, iOS 18.7, WebKit.
  · Seit der Umstellung am 02.10. (neue Aufnahmen bevorzugen MP4)
    wurde KEINE EINZIGE neue aufgenommen. Ihr Problem betrifft also
    ausschliesslich die sechs alten Dateien -- und jede kuenftige aus
    einem Firefox, der nichts anderes kann.

WAS ICH NICHT MESSEN KONNTE, und das gehoert dazu: Ob WebKit
WebM/Opus abspielen kann, laesst sich auf diesem Rechner nicht
nachsehen -- Playwrights WebKit startet hier nicht (libegl.dll
fehlt). Die Vermutung „iPhones koennen kein WebM" ist begruendet,
aber von mir nicht gemessen.

GENAU DESHALB STELLT DIE LOESUNG DIE FRAGE NICHT. Statt zu raten,
welches Geraet welchen Behaelter kann, legt der Server neben jede
Sprachnachricht eine zweite Fassung in dem Format, bei dem sich seit
zwanzig Jahren alle einig sind: AAC in MP4. Gibt es sie, wird sie
abgespielt -- bei jedem, nicht nur auf iPhones. Kein Geraetename im
Code, keine Liste, die altert.

  helfer-ffmpeg.mjs     ffmpeg finden, umwandeln, drei Ausgaenge
  beim Hochladen        nebenher, nicht davor: Die Nachricht wartet
                        nicht auf ffmpeg
  toeneNachruesten()    das Netz darunter -- fuer die sechs von
                        frueher und fuer den Fall, dass eine
                        Umwandlung einmal nicht geklappt hat
  ?form=mp4             dieselbe Route, dieselben Rechte; eine
                        zweite haette dieselbe Sichtbarkeitspruefung
                        ein zweites Mal gebraucht

ffmpeg IST ABGESPROCHEN INSTALLIERT (Debian 7.1.5, auf Nachfrage
freigegeben). Fehlt es, passiert nichts Schlimmes: Die
Sprachnachricht geht wie bisher im Originalformat hinaus, und es
steht EINMAL eine Zeile im Protokoll -- nicht bei jedem Hochladen.

DREI ENTSCHEIDUNGEN, DIE NICHT NAHELIEGEND WAREN

 1. `-movflags +faststart` IST NICHT KOSMETIK. Ohne es steht die
    Inhaltsuebersicht einer MP4 am ENDE. Das Abspielgeraet muss dann
    erst bis ans Ende lesen, bevor es anfangen kann -- genau das
    „laedt die ganze Zeit" aus Miss' Runde 5. Geprueft wird die
    Reihenfolge der Kaesten in der Datei, nicht die Zeile im Aufruf.

 2. DAS ORIGINAL BLEIBT LIEGEN. Ohne `?form=mp4` kommt weiter die
    WebM. Sie ist das, was aufgenommen wurde; sie durch eine
    Umrechnung zu ersetzen hiesse, das Original wegzuwerfen.

 3. EIN GEMEINSAMER LOESCHER. Zwei Stellen entfernen Anhaenge
    (Gespraech wegraeumen, Nachricht zuruecknehmen). Beide nahmen
    genau eine Datei -- ab heute waere bei jeder geloeschten
    Sprachnachricht eine m4a liegengeblieben, ohne Zeile, die auf
    sie zeigt. Dieselbe Luecke hatte ich gestern bei den
    Supportbildern gefunden; hier steht sie von Anfang an an EINER
    Stelle.

ZWEI FUNDE DER PRUEFUNG, BEIDE MEINE EIGENEN

 · `anhang_datei` STAND NICHT IN DER ABFRAGE der Nachrichtenliste.
   Die Funktion, die auf der Platte nachsieht, bekam deshalb nichts
   und sagte brav „gibt es keine zweite Fassung" -- fuer JEDE
   Nachricht. Alles war gebaut, nichts kam an. Gefunden hat das die
   Pruefung, nicht das Lesen.
 · Mein erster Anlauf mass die Aufnahme ueber den Knopf -- und die
   ist seit dem 02.10. schon MP4, braucht also gar keine zweite
   Fassung. Die Pruefung wurde rot und hatte recht: Gemessen werden
   muss der Fall, den es bei Miss gibt. Jetzt nimmt sie ausdruecklich
   eine WebM auf.

Dazu zweimal derselbe alte Tritt: ein Gegen-Apostroph in einem
Kommentar INNERHALB eines Template-Literals. Der Server startet dann
gar nicht. Steht jetzt als Warnung an beiden Stellen.

GEPRUEFT -- pruef-chat-anhaenge 146 -> 163 ok

  Eine echte WebM/Opus-Aufnahme (31 972 Bytes) wird hochgeladen; die
  zweite Fassung entsteht von selbst und kommt als audio/mp4 heraus,
  mit „ftyp"-Marke, mit Accept-Ranges. ffprobe sagt: Original Opus,
  Zweitfassung AAC, 2,40 s gegen 2,46 s. Die Inhaltsuebersicht steht
  bei Byte 32, die Tondaten ab 1270 -- also vorne. Beim Loeschen
  gehen beide Dateien. Gegenproben: ein Bild bekommt keine zweite
  Fassung; eine halbierte Laenge waere aufgefallen; das Original
  bleibt unter seiner eigenen Adresse abrufbar.

  Der dritte Ausgang hat einen eigenen Zaehler: Fehlt ffmpeg, steht
  „KONNTE NICHT NACHSEHEN" mit Anleitung da -- nicht gruen und nicht
  rot. Und der Block raeumt hinter sich auf: Mein Probebild liess
  eine spaetere Pruefung rot werden, weil es die Gespraechsliste
  veraendert hatte. Eine Pruefung, die den Stand fuer die naechste
  verschiebt, ist schlimmer als keine.

  Dazu gruen: pruef-struktur 102 (92 Module) · pruef-ports 10 ·
  pruef-gifs 17 · pruef-treffchat 114 · pruef-chat-optik ·
  pruef-aufbewahrung 45.

NUR DAS AGENTURHAUS? NEIN -- der Chat gehoert beiden Haeusern, und
die Aenderung gilt fuer beide gleich. Sie aendert nichts an
Sichtbarkeit oder Rechten: Wer eine Nachricht hoeren darf, hoert sie
jetzt in einem Format, das sein Geraet kann.

OFFEN BLEIBT DIE ANTWORT VON MISS. Ob es bei ihr jetzt laeuft, weiss
nur sie -- deshalb steht ihre Meldung weiter offen und nicht auf
erledigt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-03 01:41:34 +02:00

188 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
helfer-ffmpeg.mjs — eine Sprachnachricht, die JEDES Gerät abspielt
=====================================================================
WARUM ES DIESE DATEI GIBT (03.10.2026)
Miss im Support, Meldung #12 — fünf Runden lang: „Bei mir lässt
sich die Nachricht nicht abspielen." Zuletzt: „Jetzt lädt es die
ganze Zeit." Bei allen anderen ging es.
GEMESSEN, BEVOR GEBAUT WURDE:
· Alle sechs Sprachnachrichten im Haus sind `audio/webm`.
· Miss' Gerät: iPhone, iOS 18.7, WebKit.
· Seit der Umstellung am 02.10. (neue Aufnahmen bevorzugen MP4)
wurde KEINE EINZIGE neue aufgenommen — ihr Problem betrifft
also ausschließlich die sechs alten Dateien.
WAS ICH NICHT MESSEN KONNTE, und das gehört hierher: Ob WebKit
WebM/Opus abspielen kann, lässt sich auf diesem Rechner nicht
nachsehen — Playwrights WebKit startet hier nicht (`libegl.dll`
fehlt). Die Vermutung „iPhones können kein WebM" ist deshalb
begründet, aber nicht von mir gemessen.
GENAU DESHALB IST DIE LÖSUNG HIER EINE, DIE DIE FRAGE NICHT
STELLT. Statt zu raten, welches Gerät welchen Behälter kann, legt
der Server neben jede Sprachnachricht eine zweite Fassung in dem
Format, das seit zwanzig Jahren überall läuft: AAC in MP4. Wer sie
hat, bekommt sie. Kein Gerätename im Code, keine Liste, die
altert.
---------------------------------------------------------------------
WARUM EIN FREMDES PROGRAMM UND NICHT NODE
Node kann kein Audio umwandeln, und ein Paket dafür wäre die
einzige nennenswerte Abhängigkeit in einem Haus, das ohne Baulauf
auskommt. ffmpeg ist auf dem Server installiert (7.1.5, Debian) —
abgesprochen, nicht nebenbei eingerichtet.
DREI AUSGÄNGE, wie überall: umgewandelt / nicht umwandelbar /
ffmpeg nicht auffindbar. Der dritte ist KEIN Fehler: Fehlt ffmpeg,
wird die Sprachnachricht wie bisher im Originalformat
ausgeliefert. Niemand verliert etwas, was er vorher hatte — es
kommt nur niemand dazu.
===================================================================== */
import { existsSync } from "node:fs";
import { execFile } from "node:child_process";
import { delimiter, join, extname, basename, dirname } from "node:path";
/* Die Endung der Zweitfassung. AAC in MP4 — das Format, bei dem sich
iPhone, Android, Windows, Mac und jeder Browser einig sind. */
export const ZWEITFASSUNG_ENDUNG = ".m4a";
export const ZWEITFASSUNG_TYP = "audio/mp4";
/* 96 kbit/s für Sprache. Opus holt bei 32 kbit/s dasselbe heraus,
AAC braucht mehr — und eine Sprachnachricht von einer Minute wiegt
damit 700 KB statt 240. Das ist der Preis dafür, dass sie überall
läuft, und er ist es wert. Niedriger wäre hörbar dumpf; höher
brächte bei einem Handymikrofon nichts. */
const RATE = "96k";
/* Nach dieser Zeit wird abgebrochen. Eine Sprachnachricht von einer
Minute braucht auf dem Server unter einer Sekunde; wer hier in die
Frist läuft, hat ein anderes Problem, und ein hängender Prozess
wäre das schlimmere. */
const FRIST_MS = 60_000;
const BEKANNTE_ORTE = [
"/usr/bin/ffmpeg",
"/usr/local/bin/ffmpeg",
"C:/Program Files/ffmpeg/bin/ffmpeg.exe",
];
let gemerkt = null;
let schonGesagt = false;
/* IM PATH SUCHEN, OHNE `where` ODER `which` ZU BRAUCHEN — dieselbe
Überlegung wie in helfer-openssl.mjs: Beide sind selbst Programme
und können genauso fehlen. */
function imPfad() {
const pfade = (process.env.PATH || "").split(delimiter).filter(Boolean);
const namen = process.platform === "win32" ? ["ffmpeg.exe", "ffmpeg"] : ["ffmpeg"];
for (const p of pfade) {
for (const n of namen) {
const voll = join(p, n);
if (existsSync(voll)) return voll;
}
}
return null;
}
/** Wo liegt ffmpeg — oder `null`.
*
* GIBT `null` ZURUECK UND WIRFT NICHT. Anders als bei openssl ist
* das Fehlen hier kein Abbruchgrund: Die Sprachnachricht
* funktioniert auch ohne Zweitfassung, nur eben nicht überall. Ein
* geworfener Fehler würde das Hochladen scheitern lassen — und
* damit aus einer fehlenden Verbesserung einen Ausfall machen. */
export function ffmpegPfad() {
if (gemerkt !== null) return gemerkt || null;
gemerkt = imPfad() || BEKANNTE_ORTE.find((o) => existsSync(o)) || "";
if (!gemerkt && !schonGesagt) {
schonGesagt = true;
/* EINMAL SAGEN, NICHT BEI JEDER NACHRICHT. Eine Zeile, die bei
jedem Hochladen kommt, liest nach dem dritten Mal niemand
mehr — und deckt dabei die echten zu. */
console.log("[ton] ffmpeg nicht gefunden – Sprachnachrichten bekommen keine "
+ "zweite Fassung. Auf Debian: sudo apt-get install -y ffmpeg");
}
return gemerkt || null;
}
/** Wo liegt ffprobe — oder `null`.
*
* ES LIEGT IMMER NEBEN ffmpeg (dasselbe Paket, derselbe Ordner),
* deshalb wird es dort gesucht und nicht noch einmal im PATH.
* Gebraucht wird es nur von den Prüfungen: Nur ffprobe kann sagen,
* ob in der erzeugten Datei wirklich AAC steckt und ob sie so lang
* ist wie das Original. Ohne diese Frage wäre „die Datei ist da"
* ein grüner Haken über einer Datei, die niemand abspielen kann. */
export function ffprobePfad() {
const ff = ffmpegPfad();
if (!ff) return null;
const nachbar = ff.replace(/ffmpeg(\.exe)?$/i, (m) => m.replace(/ffmpeg/i, "ffprobe"));
return existsSync(nachbar) ? nachbar : null;
}
/** Der Pfad, unter dem die Zweitfassung einer Datei liegt (oder läge). */
export function zweitfassungPfad(pfad) {
return join(dirname(pfad), basename(pfad, extname(pfad)) + ZWEITFASSUNG_ENDUNG);
}
/** Braucht diese Datei überhaupt eine Zweitfassung?
*
* NEIN, WENN SIE SCHON DAS ZIELFORMAT IST. Eine m4a nach m4a zu
* wandeln kostet Rechenzeit und Qualität für genau nichts — und
* seit dem 02.10. nehmen die meisten Geräte ohnehin MP4 auf. */
export function brauchtZweitfassung(pfad) {
return extname(pfad).toLowerCase() !== ZWEITFASSUNG_ENDUNG;
}
/** Eine Tondatei in AAC/MP4 danebenlegen.
*
* Gibt ein Versprechen auf `true` (liegt jetzt da), `false` (ging
* nicht) oder `null` (ffmpeg fehlt — konnte nicht nachsehen).
*
* SIE WIRFT NIE. Der Aufrufer hängt an einem Hochladen, und ein
* Fehler beim Umwandeln darf die Nachricht nicht verhindern.
*
* `-movflags +faststart` IST NICHT KOSMETIK: Ohne es steht die
* Inhaltsübersicht einer MP4 am ENDE der Datei. Ein Abspielgerät
* muss dann erst bis ans Ende lesen, bevor es anfangen kann — bei
* einer Teilanfrage über eine mobile Verbindung ist das genau das
* „lädt die ganze Zeit", über das sich Miss beklagt hat. */
export function zweitfassungBauen(pfad) {
return new Promise((fertig) => {
const ff = ffmpegPfad();
if (!ff) return fertig(null);
const ziel = zweitfassungPfad(pfad);
/* SCHON DA HEISST FERTIG. Der Nachrüstlauf geht mehrmals über
denselben Ordner; ohne diese Zeile würde er jedes Mal alles
neu wandeln. */
if (existsSync(ziel)) return fertig(true);
execFile(ff, [
"-nostdin", "-hide_banner", "-loglevel", "error",
"-i", pfad,
/* KEIN BILD. Eine WebM kann ein Titelbild enthalten; in einer
Audiodatei hätte es nichts verloren und bringt ffmpeg bei
manchen Dateien zum Stolpern. */
"-vn",
"-c:a", "aac", "-b:a", RATE,
"-movflags", "+faststart",
/* NICHT ÜBERSCHREIBEN. Eine halbe Datei von einem
abgebrochenen Lauf soll auffallen, nicht stillschweigend
ersetzt werden -- und `existsSync` oben hat ohnehin schon
gefragt. */
"-n", ziel,
], { timeout: FRIST_MS }, (fehler) => {
if (fehler) {
console.error(`[ton] Zweitfassung fuer ${basename(pfad)} ging nicht:`,
String(fehler.message || fehler).split("\n")[0]);
return fertig(false);
}
fertig(true);
});
});
}