Video-Thumbnails mit FFmpeg und .NET automatisieren
Um ein Video-Thumbnail mit C# zu erzeugen, springen Sie zu einem Zeitstempel, codieren einen Frame und prüfen, ob tatsächlich ein Bild geschrieben wurde. FFmpeg kann ohne Frame enden, wenn Sie hinter das Videoende springen. Dieses Konsolenprojekt erstellt mit FFMpegCore ein JPEG und gibt einen Exit-Code ungleich null zurück, wenn es das Thumbnail nicht erstellen kann.
FFmpeg und das .NET-Projekt einrichten
Verwenden Sie eine Linux-Shell mit dem .NET 10 SDK
und sowohl ffmpeg als auch ffprobe in PATH.
Installieren Sie FFmpeg über Ihre Distribution oder die
FFmpeg-Downloadseite. Das folgende Beispiel erstellt ein H.264-Beispielvideo.
Ihr FFmpeg-Build benötigt daher auch den Encoder libx264.
Prüfen Sie die installierten Werkzeuge:
dotnet --version && ffmpeg -version && ffprobe -version
Führen Sie diesen Einrichtungsblock in einem Verzeichnis aus, in dem Sie Projekte ablegen. Er
legt ein neues Verzeichnis an und lässt Ihre Shell darin. Falls ein Befehl fehlschlägt, halten Sie
an und beheben Sie den Fehler, bevor Sie fortfahren. Die Operatoren &&
verhindern, dass spätere Einrichtungsbefehle nach einem Fehler ausgeführt werden.
mkdir ThumbnailDemo &&
cd ThumbnailDemo &&
dotnet new console --framework net10.0 --no-restore &&
dotnet add package FFMpegCore --version 5.2.0
Damit wird FFMpegCore 5.2.0 festgelegt, die hier verwendete Wrapper-Version. Die nativen ausführbaren FFmpeg-Dateien werden separat installiert. Für dieses Beispiel benötigen Sie weder ein Logging-Paket noch eine Bildbibliothek.
Den Thumbnail-Dienst erstellen
Speichern Sie Folgendes als ThumbnailService.cs in ThumbnailDemo.
Der Dienst akzeptiert eine lokale Videodatei, ein neues Ziel mit der Erweiterung
.jpg und einen Zeitstempel, gemessen ab dem Beginn des Videos.
Das Zielverzeichnis muss bereits existieren.
using FFMpegCore;
public static class ThumbnailService
{
public static async Task GenerateAsync(
string inputFile, string outputFile, TimeSpan captureTime)
{
if (captureTime < TimeSpan.Zero)
throw new ArgumentException("The timestamp must not be negative.");
inputFile = Path.GetFullPath(inputFile);
outputFile = Path.GetFullPath(outputFile);
if (!File.Exists(inputFile))
throw new FileNotFoundException("The input video does not exist.", inputFile);
if (!Path.GetExtension(outputFile).Equals(".jpg", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Use a .jpg output filename.");
if (File.Exists(outputFile) || Directory.Exists(outputFile))
throw new IOException("The destination already exists. Choose a new filename.");
var directory = Path.GetDirectoryName(outputFile)!;
if (!Directory.Exists(directory))
throw new DirectoryNotFoundException("The output directory does not exist.");
var temporaryFile = Path.Combine(directory, $".thumbnail-{Guid.NewGuid():N}.jpg");
try
{
await FFMpegArguments
.FromFileInput(inputFile, addArguments: options => options.Seek(captureTime))
.OutputToFile(temporaryFile, false, options => options
.WithVideoCodec("mjpeg")
.WithFrameOutputCount(1)
.WithVideoFilters(filters => filters.Scale(320, -2))
.ForceFormat("image2")
.WithCustomArgument("-map 0:v:0 -pix_fmt yuvj420p -update 1"))
.ProcessAsynchronously();
if (!File.Exists(temporaryFile) || new FileInfo(temporaryFile).Length == 0)
throw new InvalidOperationException("No frame was produced. Try an earlier timestamp.");
var image = await FFProbe.AnalyseAsync(temporaryFile);
if (!image.VideoStreams.Any(stream =>
stream.CodecName == "mjpeg" && stream.Width == 320 && stream.Height > 0))
throw new InvalidOperationException("The output is not the expected JPEG thumbnail.");
File.Move(temporaryFile, outputFile, overwrite: false);
}
finally
{
File.Delete(temporaryFile);
}
}
}
So funktioniert der Code
Die Eingabe Seek setzt -ss vor
-i. Beim Transkodieren springt FFmpeg zu einem früheren Sprungpunkt
und decodiert und verwirft Frames bis zur angeforderten Position, wie in der
Dokumentation zu Suchsprüngen beschrieben. Die explizite Angabe
-map 0:v:0 wählt den ersten Videostream aus. Bei einer reinen Audiodatei kann
nicht stillschweigend ein Audiostream an dessen Stelle treten.
Die Ausgabe fordert einen MJPEG-Frame mit einem JPEG-Pixelformat mit vollem Wertebereich an.
Die Skalierung auf 320:-2 berechnet aus den Proportionen der Quelle eine
gerade Höhe. So erhält das folgende Beispiel mit quadratischen Pixeln die Größe 320 × 180,
statt auf 320 × 240 gestreckt zu werden. Die
Dokumentation zum scale-Filter erklärt den negativen Höhenwert.
Verwenden Sie für diese Anleitung gewöhnliches SDR-Video mit quadratischen Pixeln. Für
HDR-Tonemapping und die Verarbeitung anamorpher Videos ist eine separate Filterwahl nötig.
Die image2-Option update behandelt die
temporäre Ausgabe als einen einzelnen Dateinamen. Nach Abschluss von FFmpeg prüft der Dienst die
Dateigröße und ermittelt Codec und Abmessungen. Erst dann verschiebt er die Datei an Ihr Ziel.
Das Verschieben ohne Überschreiben
verweigert auch ein Ziel, das während der Ausführung von FFmpeg erstellt wurde. Bei regulärem
Abschluss oder einem Fehler entfernt der Block finally die temporäre Datei.
Ein zwangsweise beendeter Prozess kann sie zurücklassen.
Das Konsolenprogramm ausführen
Ersetzen Sie die generierte Datei Program.cs durch diesen vollständigen
aufrufenden Code. Er akzeptiert Sekunden mit einem Dezimalpunkt, etwa
0.25, und schreibt Diagnosemeldungen in die Standardfehlerausgabe.
Der Rückgabewert wird zum Exit-Code des Prozesses: null bei Erfolg, zwei bei ungültigen Argumenten
und eins bei einem fehlgeschlagenen Vorgang.
using System.Globalization;
if (args.Length != 3 ||
!double.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var seconds) ||
!double.IsFinite(seconds) || seconds < 0)
{
Console.Error.WriteLine("Usage: dotnet run -- <input-video> <output.jpg> <seconds>");
return 2;
}
try
{
await ThumbnailService.GenerateAsync(args[0], args[1], TimeSpan.FromSeconds(seconds));
Console.WriteLine($"Created {Path.GetFullPath(args[1])}");
return 0;
}
catch (Exception error)
{
Console.Error.WriteLine($"Thumbnail failed: {error.Message}");
return 1;
}
Erstellen Sie weiterhin in ThumbnailDemo ein viersekündiges Testvideo:
zwei Sekunden Rot, gefolgt von zwei Sekunden Blau. Falls sample.mp4 bereits
existiert, beendet die anfängliche Prüfung diesen Block mit einem Status ungleich null und
bewahrt die Datei. Verwenden Sie für ein neues Beispiel ein neues Projektverzeichnis.
test ! -e sample.mp4 &&
ffmpeg -n -f lavfi -i "color=c=red:s=640x360:r=25:d=2" \
-f lavfi -i "color=c=blue:s=640x360:r=25:d=2" \
-filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" \
-map "[v]" -c:v libx264 -pix_fmt yuv420p sample.mp4
Erfassen Sie den Frame bei drei Sekunden:
dotnet run -- sample.mp4 thumbnail.jpg 3
Öffnen Sie thumbnail.jpg: Das Bild sollte blau sein und 320 × 180 Pixel messen.
Sie können es auch mit ffprobe untersuchen und anschließend mit FFmpeg
decodieren, ohne ein weiteres Bild zu schreiben:
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height \
-of default=noprint_wrappers=1 thumbnail.jpg &&
ffmpeg -v error -xerror -i thumbnail.jpg -f null -
Die Analyse meldet codec_name=mjpeg, width=320 und
height=180. Erfolgreiches Decodieren erzeugt keine Meldung.
Ersetzen Sie sample.mp4 durch Ihren eigenen lokalen Videopfad und wählen Sie
einen Zeitstempel, an dem ein Frame existiert. Setzen Sie Pfade mit Leerzeichen in Anführungszeichen.
Jeder Aufruf benötigt einen neuen Ausgabenamen, auch wenn Sie denselben Zeitstempel wiederholen.
Fehlende Frames und wiederholte Ausführungen behandeln
Ein kurzer Clip kann vor dem angeforderten Zeitstempel enden. Versuchen Sie
0 für seinen ersten Frame oder einen kleineren Zeitstempel mit
Nachkommastellen. Ein Sprung zum oder hinter das Ende kann entweder FFmpeg fehlschlagen lassen
oder ohne Bild enden. Beide Fälle gelten als fehlgeschlagener Vorgang. Der Dienst ersetzt den
Zeitpunkt nicht stillschweigend durch einen anderen.
Für das viersekündige Beispiel muss dieser Befehl einen Status ungleich null zurückgeben und darf keine Zieldatei hinterlassen:
dotnet run -- sample.mp4 beyond-end.jpg 10
Ein unlesbares Video, eine fehlende Eingabe oder eine reine Audiodatei führt ebenfalls zum
Fehlschlag. Wenn die Werkzeuge nicht gefunden werden, prüfen Sie PATH
in derselben Shell, die das Programm ausführt. Falls ein Ziel bereits existiert, bewahrt das
Programm dessen Bytes und schlägt fehl. Wählen Sie für einen erneuten Versuch einen anderen Namen.
Dies schützt auch die Eingabe, falls Sie sie versehentlich als Ziel verwenden. Fehlgeschlagene
Argumentprüfungen lassen vorhandene Dateien unverändert.
Für mehrere Vorschaubilder rufen Sie GenerateAsync nacheinander mit unterschiedlichen
Zeitstempeln und Ausgabenamen auf. Lassen Sie eine Ausnahme bis zum aufrufenden Code durchreichen,
damit eine Stapelverarbeitung nach dem Überspringen eines fehlgeschlagenen Thumbnails keinen
vollständigen Erfolg melden kann. Im Beispiel mit einem einzelnen Frame bleiben Ablaufplanung
und Nebenläufigkeit außerhalb des Dienstes.
Die eingesetzte Laufzeitumgebung prüfen
Diese Anleitung wurde unter Linux mit .NET SDK 10.0.111, FFMpegCore 5.2.0 und FFmpeg/ffprobe 6.1.1 und 9.0.1 überprüft. Sie belegt keine Kompatibilität mit jeder dazwischenliegenden Version, älteren .NET-Versionen, Windows oder macOS. Halten Sie beide nativen ausführbaren Dateien für das Konto verfügbar, das Ihr Programm ausführt. Führen Sie das Beispiel und den Fehlerfall erneut aus, wenn Sie den FFmpeg-Build Ihrer Bereitstellung ändern.
Wenn Sie einen gehosteten Thumbnail-Workflow benötigen, finden Sie weitere Informationen in der Dokumentation zum Robot für Video-Thumbnails. Bewahren Sie für dieses lokale Projekt das Beispielvideo auf. Damit prüfen Sie schnell, ob ein Update der Laufzeitumgebung weiterhin den erwarteten Frame erzeugt.
