Videos auf dem Handy mit ffmpeg zusammenfügen: gemischte Seitenverhältnisse in einem Vlog
Die Grundidee von Anılog ist diese: eine Freundesgruppe nimmt zu selbst gewählten Zeiten ein paar Sekunden auf, und am Ende des Tages wird alles zu einem Vlog.
Klingt einfach, aber die Eingabe ist unvorhersehbar. An einem einzigen Tag kann alles davon zusammenkommen:
- Ein hochkant aufgenommener 2-Sekunden-Clip
- Ein quer aufgenommener 10-Sekunden-Clip
- Ein einzelnes Foto
- Clips mit und ohne Ton
Alles muss sich in einem 1920×1080-Video treffen und dabei gut aussehen. Und das läuft auf dem Handy — ffmpeg auf einem Server nutze ich nicht.
Warum auf dem Handy?
Serverseitig zusammenzufügen wäre einfacher gewesen. Drei Gründe dagegen:
- Kosten. Auf Shared Hosting ist ffmpeg entweder nicht verfügbar oder dafür viel zu langsam.
- Speicher. Die Vlog-Datei wird nie hochgeladen. Ist die größte Datei einer App der Vlog, ist ihn gar nicht erst hochzuladen die größte mögliche Ersparnis.
- Datenschutz. Den Satz "der Tages-Vlog verlässt dein Gerät nie" schreiben zu können, ist gerade deshalb wertvoll, weil er stimmt.
Auf der Flutter-Seite nutze ich das Paket ffmpeg_kit_flutter_new.
Schritt 1: jeden Clip in einen gemeinsamen Rahmen setzen
Der klassische Weg, unterschiedliche Seitenverhältnisse zu kombinieren, ist schwarzer Rand an den kurzen Seiten. Das sieht schlecht aus.
Stattdessen nutze ich unscharfe Ränder: eine vergrößerte, weichgezeichnete Kopie desselben Bildes wird zum Hintergrund, das echte Bild sitzt mittig darauf. Der Effekt, den man von Instagram und TikTok kennt.
Die Filterkette:
[0:v]split=2[zemin][on];
[zemin]scale=1920:1080:force_original_aspect_ratio=increase,
crop=1920:1080,
gblur=sigma=28,
eq=brightness=-0.18[bulanik];
[on]scale=1920:1080:force_original_aspect_ratio=decrease[sigan];
[bulanik][sigan]overlay=(W-w)/2:(H-h)/2,
fps=30,setsar=1,
format=yuv420p[v]Gelesen:
split=2teilt dieselbe Eingabe in zwei Zweige.- Der Hintergrundzweig skaliert mit
increase, sodass er den Rahmen überfüllt, undcropschneidet zu. So bleibt keine Lücke. gblur=sigma=28zeichnet weich,eq=brightness=-0.18dunkelt ab. Ohne das Abdunkeln konkurriert der Hintergrund mit dem Vordergrund.- Der Vordergrundzweig skaliert mit
decrease, sodass er in den Rahmen passt. overlayzentriert.
setsar=1 ist Pflicht. Manche Handys nehmen mit nicht-quadratischem Pixelseitenverhältnis auf; ohne Zurücksetzen wird das Ergebnis horizontal gestaucht.
Fotos laufen durch dieselbe Kette, nur mit -loop 1 -t <Sekunden> am Eingang.
Schritt 2: den Text einbrennen — samt Sonderzeichen
Über jedem Clip stehen der Name, die Aufnahmezeit und eine optionale Bildunterschrift, gezeichnet mit dem drawtext-Filter.
Hier habe ich früh eine Lektion gelernt: Nutzertext niemals direkt in den Filterstring schreiben.
drawtext=text='öğle molası uzadı' ← brüchigDie Filterkette ist eine einzige Zeichenkette, und : ' % \ darin müssen alle maskiert werden. Eine Bildunterschrift kann jedes davon enthalten. Ein einziges Apostroph zerlegt den ganzen Befehl.
Die Lösung heißt textfile:
drawtext=fontfile=/.../Manrope.ttf:textfile=/.../untertitel.txt:x=76:y=h-th-64Der Text landet als UTF-8 in einer temporären Datei, ffmpeg liest ihn von dort. Das Maskierungsproblem verschwindet vollständig.
Zweiter Punkt: ohne fontfile greift ffmpeg auf eine Systemschrift zurück, und Sonderzeichen erscheinen als Kästchen. Ich kopiere die eigenen Schriften der App (Manrope, Fredoka) aus dem Bundle in ein temporäres Verzeichnis und übergebe den Pfad.
Schritt 3: die Clips aneinanderhängen
Sind alle Clips auf dieselbe Auflösung, Bildrate und dasselbe Audioformat normalisiert, ist das Zusammenfügen billig:
-f concat -safe 0 -i liste.txt -c copy ausgabe.mp4Der concat-Demuxer mit -c copy kodiert nicht neu — das dauert Sekunden. Aber nur, wenn die Eingaben wirklich übereinstimmen. Ein Clip mit abweichender Bildrate, und der Ton läuft aus dem Bild. Den Normalisierungsschritt nicht überspringen.
Der alternative concat-Filter kodiert alles neu und ist auf einem Handy deutlich langsamer.
Schritt 4: Musik hinzufügen
Man kann einen Titel aus den eigenen Dateien wählen und ihn gegen den Clip-Ton abmischen. Drei Probleme:
Der Titel kann kürzer sein als der Vlog. Am Eingang in Schleife legen:
-stream_loop -1 -i musik.mp3Abruptes Anfangen und Enden ist unangenehm. Eine Sekunde ein, zwei Sekunden aus:
afade=t=in:st=0:d=1,
afade=t=out:st=<Ende-2>:d=2Die beiden Tonspuren müssen sich mischen:
[k][m]amix=inputs=2:duration=first:dropout_transition=0[a]duration=first ist wichtig: die geloopte Musik gilt als unendlich lang, ohne first endet die Ausgabe nie. dropout_transition=0 verhindert, dass eine Spur laut aufspringt, wenn die andere still wird.
Schritt 5: Kompression — und messen
Speicher ist endlich, hochgeladene Clips werden also komprimiert. Bleibt die Frage: wie viel Qualität hat das gekostet?
"Sah für mich gut aus" ist keine Messung. ffmpeg kann es beantworten:
ffmpeg -i original.mp4 -i komprimiert.mp4 \
-lavfi "psnr;[0:v][1:v]ssim" -f null -Bei CRF 23 mit Herunterskalierung auf 720p, an einem echten Nutzerclip:
| Maß | Ergebnis | Lesart |
|---|---|---|
| PSNR | 43,35 dB | Über 40 dB gilt allgemein als visuell nicht unterscheidbar |
| SSIM | 0,974 | 1,0 wäre exakte Übereinstimmung |
Diese Zahlen machten die Entscheidung leicht. Die Dateigröße sank deutlich, der Qualitätsverlust blieb messbar vernachlässigbar.
Empfehlung: wählen Sie Ihre Kompressionseinstellung nicht mit "sieht gut aus". Zwei Befehle, zwei Zahlen — und eine Entscheidung, die sich verteidigen lässt.
Fünf Dinge, die man vermeiden sollte
- Schwarze Ränder. Unscharfe Ränder kosten fast dasselbe und sehen weit besser aus.
- Nutzertext über
drawtext=text=.textfileverwenden. fontfileweglassen. Sonderzeichen werden zu Kästchen.concat -c copyohne Normalisierung. Der Ton läuft aus dem Bild.setsar=1vergessen. Auf manchen Geräten wird das Bild gestaucht.
Die vollständige Architektur von Anılog und der Zusammenhang dieser Entscheidungen stehen in einem eigenen Beitrag.
Verwandte Artikel
Flutter-Kamera ohne erzwungene Ausrichtung: drei Fehler und ihre Lösungen
Eine gedrehte Vorschau, ein Einfrieren beim Aufnahmestart und eine tote Kamera nach dem Öffnen der Mitteilungszentrale. Die drei konkreten Fehler beim Bau des Kamerabildschirms von Anılog in Flutter — und der Code, der sie behoben hat.
Firebase Auth mit eigenem PHP-Server: Token-Prüfung und Kontolöschung ohne Composer
Identität bei Firebase, Daten auf dem eigenen Server. Wie entscheidet PHP, dem eingehenden Token zu trauen? RS256-Prüfung auf Shared Hosting ohne Composer — und der Kontolöschungs-Ablauf, den beide App-Stores verlangen.
Dart Interface und implements – Klassen um Fähigkeiten erweitern
Lernen, wie wir unseren Klassen mit abstrakten Klassen und dem Schlüsselwort implements zusätzliche Fähigkeiten geben.