clipper
Zieht mit ffmpeg eine gewünschte Anzahl Screenshots über die Gesamtlaufzeit eines
Videos.
clipper -n 10 -i holiday.mp4
Installation
Voraussetzung sind ffmpeg und ffprobe im PATH. Auf macOS:
brew install ffmpeg
Bauen:
make build # erzeugt bin/clipper
make install # nach $GOPATH/bin
Verwendung
clipper -n <anzahl> [flags] -i <video> [weitere videos ...]
| Flag |
Bedeutung |
-n |
Anzahl Screenshots pro Video (Pflicht, > 0) |
-i |
Videodateien; wiederholbar oder mehrere Dateien hintereinander |
-o |
Ausgabeverzeichnis, wird bei Bedarf angelegt (Default: aktuelles Verzeichnis) |
-prefix |
Dateinamen-Prefix (Default: shot, bei mehreren Videos der jeweilige Videoname) |
-format |
png (Default), jpg oder webp |
-quality |
1–100, nur für jpg und webp (Default: 90) |
-jobs |
parallele ffmpeg-Prozesse (Default: min(GOMAXPROCS, 4)) |
-random |
zufällige Position je Abschnitt statt gleichmäßigem Raster |
-seed |
Seed für -random; ohne Angabe wird einer erzeugt und ausgegeben |
-version |
Version ausgeben |
Wasserzeichen, Verlauf und Vignette sind drei optionale Nachbearbeitungsschritte,
alle standardmäßig aus:
| Flag |
Default |
Bedeutung |
-watermark |
aus |
PNG-Logo in eine Ecke legen; leerer Wert sucht ./logo.png |
-watermark-size |
10 |
Prozent der Bildgröße; lange Logokante, in jedem Format gleich |
-watermark-margin |
2 |
Prozent der Bildgröße Abstand zu den Rändern |
-watermark-opacity |
100 |
1–100, wirkt zusätzlich zur Transparenz des PNG |
-watermark-position |
top-right |
top-right, top-left, bottom-right, bottom-left |
-gradient |
aus |
Farbverlauf am unteren Bildrand einblenden |
-gradient-height |
33 |
Prozent der Bildhöhe, 1 bis 100 |
-gradient-color |
#000000 |
Hex-Farbe, auch kurz (#000) und ohne # |
-gradient-opacity |
95 |
1–100, Deckkraft in der untersten Bildzeile |
-vignette |
aus |
Bildränder abdunkeln, immer schwarz |
-vignette-radius |
65 |
0–100, freie Zone in Prozent des Weges von der Mitte zur Ecke |
-vignette-softness |
50 |
0–100 in derselben Einheit, Breite des Übergangs; 0 ergibt eine harte Kante |
-vignette-opacity |
40 |
1–100, Deckkraft am Ende des Übergangs |
Beispiele:
clipper -n 10 -i holiday.mp4
clipper -n 5 -o shots -format jpg -quality 85 -i a.mp4 b.mp4
clipper -n 8 -random -seed 42 -i holiday.mp4
clipper -n 6 -watermark assets/logo.png -gradient -i holiday.mp4
clipper -n 6 -vignette -i holiday.mp4 # dezente Abdunklung der Ecken
clipper -n 6 -vignette -vignette-radius 40 -vignette-opacity 70 -i holiday.mp4
clipper -n 6 -watermark "" -i holiday.mp4 # nimmt ./logo.png
Zwei Eigenheiten der Overlay-Flags: Der Wert von -watermark ist ein Pfad, ein
leerer Wert (-watermark "") heißt „nimm logo.png aus dem Verzeichnis, in dem
clipper aufgerufen wurde". Und Unterflags ohne ihr Hauptflag werden ignoriert —
-gradient-height 20 ohne -gradient tut also nichts.
Ein Hinweis zur Argumentreihenfolge: -i sammelt alle folgenden Argumente als
Videos ein. Nach dem zweiten Video kann daher kein Flag mehr folgen —
clipper -n 2 -i a.mp4 b.mp4 -format jpg bricht mit einer entsprechenden
Meldung ab. Direkt hinter einem einzelnen -i-Wert sind Flags dagegen erlaubt.
Ausgabe und Exit-Codes
Fortschritt, Warnungen und Fehler gehen auf stderr, die Zusammenfassung auf
stdout. Die Fortschrittszeilen erscheinen bei paralleler Ausführung in
beliebiger Reihenfolge und nennen deshalb immer Video und Index.
| Code |
Bedeutung |
| 0 |
alles geschrieben |
| 1 |
mindestens ein Screenshot oder Video fehlgeschlagen, oder Umgebung unbrauchbar |
| 2 |
Kommandozeile falsch |
| 130 |
mit Strg+C abgebrochen (bereits erzeugte Bilder bleiben erhalten) |
Verhalten im Detail
Verteilung. Standardmäßig liegt Screenshot i bei (i+1)·Dauer/(n+1). Das
ist symmetrisch und vermeidet den allerersten und allerletzten Frame, die häufig
schwarz sind. Mit -random wird das Video in n gleich große Abschnitte
geteilt und in jedem eine zufällige Position gezogen (stratified sampling); der
verwendete Seed wird ausgegeben, damit ein Lauf wiederholbar ist.
Seeking. -ss steht vor -i, also Input-Seeking: ffmpeg springt zum
Keyframe und dekodiert von dort bis zur exakten Position. Das ist auf langen
Videos um Größenordnungen schneller als Output-Seeking und trotzdem framegenau.
Schwarzbild-Erkennung. Jeder geschriebene Screenshot wird in Go analysiert
(mittlere Helligkeit und Standardabweichung der Luminanz). Ist das Bild
praktisch schwarz oder völlig einfarbig, wird eine Ersatzposition im selben
Abschnitt genommen — insgesamt bis zu drei Versuche. Bleibt es dabei, wird das
letzte Bild behalten und eine Warnung ausgegeben.
Wasserzeichen, Verlauf und Vignette. Alle drei sind Nachbearbeitungsschritte in
reinem Go, keine ffmpeg-Filter. Ist einer davon aktiv, ändert sich die Reihenfolge
pro Screenshot:
- ffmpeg extrahiert den Frame verlustfrei als PNG in eine versteckte
Zwischendatei neben dem Ziel (
.shot_001.jpg.4711-8f2ca931.raw.png; Prozess-ID und
Zufallsanteil im Namen verhindern, dass sich zwei gleichzeitige Läufe im selben
Verzeichnis die Datei wegziehen),
- die Schwarzbild-Erkennung prüft diesen rohen Frame,
- Vignette, dann Verlauf, dann Logo werden aufgelegt,
- Go schreibt das Endbild im gewünschten Format und löscht die Zwischendatei.
Die Ebenenfolge in Schritt 3 ist bewusst so: die Vignette ahmt ein Objektiv nach
und liegt deshalb unten, ein farbiger Verlauf gehört darüber, und das Logo kommt
zuletzt, damit es in seiner Ecke nicht mitabgedunkelt wird und lesbar bleibt. Weil
es getrennte Durchgänge über dasselbe Bild sind, addiert sich die Wirkung dort, wo
sich zwei Ebenen überlappen — mit -vignette und -gradient-height 100 wird der
untere Bildrand also von beiden abgedunkelt.
Die Zwischendatei gehört dem Nachbearbeitungsschritt: sie wird auch dann entfernt,
wenn das Kodieren scheitert oder ein Wiederholungsversuch nach einem bereits
erfolgreichen Versuch fehlschlägt. Nach einem Lauf bleibt nichts liegen.
Die Prüfung läuft also bewusst vor den Overlays. Andernfalls würde der Verlauf
die Statistik verfälschen: schon ein Verlauf über das voreingestellte Drittel der
Höhe erzeugt allein eine Standardabweichung von etwa 0,16, und mit -gradient-height 100 wird kein Bild mehr als schwarz erkannt. Frames, die nach drei Versuchen
einfarbig bleiben, werden unverändert durchgeschrieben und bekommen keine Overlays.
Wie die Logogröße rechnet. Bezugspunkt ist nicht eine Bildkante, sondern die
Bildgröße als solche: das geometrische Mittel der beiden Kanten, also
√(Breite·Höhe). -watermark-size ist der Anteil davon, den die lange Kante des
Logos einnimmt, und das Logo wird unter Beibehaltung seines Seitenverhältnisses in
eine quadratische Box dieser Kantenlänge eingepasst.
Der Grund ist Vorhersagbarkeit über Bildformate. Bezogen auf die Breite ergäbe
-watermark-size 10 auf 1920x1080 ein 192 px breites Logo, auf 1080x1080 aber nur
108 px — derselbe Wert würde in jedem Format anders wirken. Mit dem geometrischen
Mittel belegt das Logo in 1:1, 9:16 und 16:9 denselben Anteil des Bildes, und zwar
für jeden Wert: der Flächenanteil ist (Prozent/100)² · Logohöhe/Logobreite und
enthält Breite, Höhe und Seitenverhältnis des Bildes überhaupt nicht.
-watermark-margin folgt demselben Bezug, sonst wäre die Größe formatunabhängig,
der Abstand zur Ecke aber nicht.
Eine Ausnahme gibt es am oberen Ende: die Box wird auf die kürzere Bildkante
gedeckelt, damit ein Logo nie größer als das Bild wird. Das greift erst weit
jenseits sinnvoller Wasserzeichengrößen, auf 16:9 etwa ab 75 Prozent.
Das Logo wird pro Lauf einmal dekodiert und pro Bildauflösung einmal skaliert
(Seitenverhältnis bleibt, nie hochskaliert). Die Quelldatei wird ausschließlich
gelesen. Wäre das skalierte Logo kleiner als 8 px, wird es weggelassen und einmal
pro Auflösung gewarnt — bei -watermark-size 5 und einem 320x240-Video passiert
genau das, dort bleiben nur 13x5 Pixel übrig.
Wie die Vignette rechnet. Der Abstand eines Pixels zur Bildmitte wird pro Achse
auf die halbe Bildbreite beziehungsweise Bildhöhe bezogen. Dadurch sind die Linien
gleicher Abdunklung Ellipsen in der Form des Bildes: ein Kreis würde auf 16:9 die
linken und rechten Ränder viel stärker abdunkeln als oben und unten. In dieser
Einheit liegt eine Kantenmitte bei 1 und eine Ecke bei √2, weshalb die Ecken von
sich aus stärker abdunkeln — das ist der Effekt, den man von einem Objektiv kennt.
-vignette-radius ist die freie Zone, gemessen als Anteil des Weges von der Mitte
zur Ecke. Bei 100 reicht sie bis in die Ecke, es passiert also nichts; das ist
kein Fehler, sondern ein stilles Nichtstun, und clipper überspringt dann auch die
Nachbearbeitung. -vignette-softness ist die Breite des Übergangs in derselben
Einheit. Beide dürfen 0 sein, und ihre Summe darf 100 überschreiten — dann ist der
Abfall in der Ecke noch nicht fertig und die Vignette bleibt entsprechend dezent.
Genau das tun die Voreinstellungen: 65 und 50 ergeben in den Ecken etwa 31
Prozent Abdunklung und an den Kantenmitten praktisch nichts.
Der Übergang ist eine Smoothstep-Kurve und keine Gerade. Das ist der Unterschied zu
einem radialen Verlauf: eine lineare Rampe hat am Beginn des Abfalls einen Knick,
und den sieht das Auge als Ring, obwohl der Sprung zwischen zwei Nachbarpixeln weit
unter einer der 255 Stufen liegt. Bei Smoothstep ist die Steigung an beiden Enden
null, der Anfang des Abfalls ist damit unsichtbar.
Die Maske hängt nur von der Bildgröße ab, nie vom Bildinhalt, und wird deshalb pro
Auflösung einmal berechnet und für alle Screenshots wiederverwendet — genau wie das
skalierte Logo, im selben Cache. Das ist die teure Hälfte des Effekts: auf 1080p
kostet der Aufbau der Maske rund 13 ms, das Auflegen danach etwa 4 ms
(go test -bench Vignette ./internal/overlay/).
Zwei Nebenwirkungen: Mit Overlays kodiert Go das Bild statt ffmpeg. Bei PNG ist
das Ergebnis pixelgleich, aber die Datei kann eine andere Größe haben; bei JPEG
bedeutet -quality dann die Skala von image/jpeg statt der von ffmpeg — gleiche
Qualitätsstufe, nicht bitgleiches Ergebnis. Und -format webp lässt sich nicht
mit Overlays kombinieren, weil Go keinen webp-Encoder hat; der Preflight lehnt das
mit einer klaren Meldung ab.
Fehler. Ein kaputtes Video oder ein fehlgeschlagener Screenshot bricht den
Lauf nicht ab. Alles wird gesammelt und am Ende als Liste ausgegeben, der
Exit-Code ist dann 1.
Überschreiben. Vorhandene Dateien werden ohne Rückfrage überschrieben — aber
erst, wenn das neue Bild vollständig ist: ffmpeg schreibt in eine temporäre Datei
neben dem Ziel, die nur bei Erfolg an ihren Platz verschoben wird. Eine
fehlgeschlagene Extraktion zerstört also keinen vorhandenen Screenshot und
hinterlässt keine halben Dateien. Tragen mehrere Videos den gleichen Basisnamen,
würden sich ihre Screenshots gegenseitig überschreiben; das lehnt der Preflight
ab.
Preflight. Vor dem ersten ffmpeg-Aufruf werden geprüft: ffmpeg und
ffprobe im PATH, Existenz und Art der Eingabedateien, Basisnamen-Kollisionen,
Beschreibbarkeit des Ausgabeverzeichnisses (per Probedatei, auch für das
aktuelle Verzeichnis) und ob der lokale ffmpeg-Build den gewünschten Encoder
überhaupt hat. Der Homebrew-Build kann zum Beispiel oft kein webp — das sagt
clipper dann klar, statt ffmpeg kryptisch scheitern zu lassen.
Bei der Art der Eingabedatei besteht clipper auf einer regulären Datei. Eine
Named Pipe, ein Socket oder eine Gerätedatei ist lesbar und kein Verzeichnis, würde
also durchrutschen — aber ffprobe blockiert darauf endlos, und der Lauf hängt ohne
jede Ausgabe. Das ist das unangenehmste Fehlerbild überhaupt, deshalb wird es vorher
abgelehnt und die Meldung nennt die Art der Datei.
Als zweite Sicherung ist jeder ffprobe-Aufruf zeitlich begrenzt, voreingestellt auf
60 Sekunden. Metadaten zu lesen dauert auch bei sehr langen Videos nur Bruchteile
davon; die Grenze existiert allein, damit ein Sonderfall, den die Preflight-Prüfung
nicht kennt — etwa eine Datei auf einem toten Netzlaufwerk — nicht denselben stummen
Hänger erzeugt. Das Extrahieren selbst bekommt bewusst keine Grenze, weil dort lange
Laufzeiten auf großen Dateien und langsamen Medien legitim sind.
Ebenfalls vorab geprüft wird die Länge der Dateinamen. Ein Dateiname darf 255 Bytes
haben, und mit aktiven Overlays gehen davon 25 Bytes für die Zwischendatei ab. Ein
sehr langer -prefix kann daher ohne Overlays funktionieren und mit ihnen nicht —
das sagt clipper jetzt vorher und in einem Satz, statt bei jedem Screenshot einzeln
mit dem Pfad der versteckten Zwischendatei zu scheitern.
Aufbau
cmd/clipper main: Signalbehandlung, Exit-Code
internal/cli Flags, Preflight, Wiring, Bericht
internal/clipper Domäne: Planung, Orchestrierung, Ports
internal/ffmpeg Adapter für ffmpeg und ffprobe
internal/imgstat Schwarzbild-Erkennung (reines Go)
internal/overlay Vignette, Verlauf und Wasserzeichen (reines Go)
assets/logo.png Beispiellogo für -watermark
Die Domäne startet keine Prozesse und dekodiert keine Bilder. Sie definiert fünf
Ports — Prober, Extractor, Analyzer, Compositor, Reporter — und wird im
Test gegen handgeschriebene Doubles in internal/clipper/clippertest gefahren
(TDD im London Style). Sie kennt die Formatnamen und deren ffmpeg-Encoder
(Format.Encoder()), damit der Preflight vorab prüfen kann, ob der lokale Build
sie schreiben kann; alles andere über ffmpeg liegt im Adapter. Ist kein
Compositor verdrahtet, schreibt ffmpeg das Endbild direkt und es findet keine
Nachbearbeitung statt — die Domäne selbst weiß nichts über Logos oder Farben, die
Overlay-Konfiguration steckt im Adapter.
Die Adapter werden gegen ein schmales Runner-Interface getestet, sodass kein Test
der Adapterlogik einen echten Prozess startet — nur der Runner selbst wird gegen
echo geprüft. Die ffprobe-Fixtures in internal/ffmpeg/testdata sind echte
Ausgaben.
Weil Mocks nicht beweisen, dass die zusammengebauten ffmpeg-Argumente
funktionieren, gibt es zusätzlich Integrationstests hinter dem Build-Tag
integration. Sie erzeugen ihre Videos selbst per ffmpeg -f lavfi — inklusive
eines Videos mit zwei Sekunden schwarzem Vorspann, um die Schwarzbild-Erkennung
im Zusammenspiel zu prüfen. Für die Overlays vergleichen sie zwei Läufe des
gleichen Frames pixelweise: außerhalb von Verlaufsband und Logo-Rechteck muss das
Bild identisch bleiben, und bei der Vignette muss das mittlere Pixel unverändert
sein, während die Ecken messbar dunkler werden und die Kantenmitten es nicht sind.
Entwicklung
make test # Unit-Tests
make test-race # mit Race Detector
make test-integration # echtes ffmpeg
make check # fmt + vet + race + integration
Lizenz
MIT, siehe LICENSE.
ffmpeg wird nur als eigener Prozess aufgerufen und nicht eingebunden, seine Lizenz
gilt also nur für die von Ihnen installierte ffmpeg-Version, nicht für clipper.