Videos effizient mit FFmpeg und JavaCV in Java aufteilen
Beim Aufteilen eines Videos müssen Audio und Wiedergabe-Timing erhalten bleiben. Die Anzahl der
Aufrufe von FFmpegFrameGrabber.grab() misst nicht die Videodauer: Ein Aufruf kann einen Audioblock
oder einen Videoframe zurückgeben. Variable Bildraten machen auch Berechnungen anhand der Frame-Anzahl
unzuverlässig.
Dieses Beispiel untersucht die Eingabe mit JavaCV und teilt komprimierte Pakete mit der mitgelieferten ausführbaren FFmpeg-Datei auf. FFmpeg verwaltet die Zeitstempel und die Verschachtelung der Audiodaten. Keine Shell interpretiert den Namen der Eingabedatei.
Warum Videos aufteilen?
Segmente sind für die Bearbeitung, die Verteilung von Downloads und die unabhängige Verarbeitung nützlich. Einzelne MP4-Segmente bilden noch keine Präsentation für adaptives Streaming; HLS oder DASH benötigt zusätzlich ein Manifest und entsprechend codierte Qualitätsstufen.
Ihre Java-Umgebung einrichten
Verwenden Sie JDK 21 und Maven. Speichern Sie Folgendes als
pom.xml und legen Sie anschließend die weiter unten gezeigte Datei
VideoSplitter.java unter src/main/java/ ab. Diese Versionsfestlegungen
verwenden JavaCV 1.5.14 und FFmpeg 8.1.2-1.5.14.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>video-splitter</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<javacpp.platform>macosx-arm64</javacpp.platform>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacv</artifactId><version>1.5.14</version>
<exclusions><exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion></exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacpp</artifactId><version>1.5.14</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>javacpp</artifactId><version>1.5.14</version>
<classifier>${javacpp.platform}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>ffmpeg</artifactId><version>8.1.2-1.5.14</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId><artifactId>ffmpeg</artifactId><version>8.1.2-1.5.14</version>
<classifier>${javacpp.platform}</classifier>
</dependency>
</dependencies>
<build><plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins></build>
</project>
Setzen Sie javacpp.platform auf Ihre tatsächliche Plattform, beispielsweise
linux-x86_64 statt macosx-arm64. Dadurch werden nur die benötigten
nativen Artefakte heruntergeladen. Informationen zu unterstützten Plattformen finden Sie in der
Installationsanleitung für JavaCV.
ffmpegframegrabber und ffmpegframerecorder verstehen
FFmpegFrameGrabber kann Bilder und Samples decodieren. FFmpegFrameRecorder.record(frame)
codiert diese decodierten Frames erneut, selbst wenn Sie den Eingabecodec auswählen. Die Auswahl eines
Codecs bewirkt kein direktes Kopieren der Streams.
Zum Aufteilen ohne visuelle Bearbeitung verwendet der folgende Unterprozess den Paketverarbeitungspfad
von FFmpeg mit -c copy. Er erhält den ersten Videostream und, falls vorhanden,
den ersten Audiostream. Zusätzliche Audiospuren, Untertitel, Kapitel, Rotationsmetadaten und beliebige
Containermetadaten werden in diesem Beispiel nicht garantiert erhalten. Prüfen Sie diese separat,
bevor Sie die Stream-Zuordnung erweitern.
Schritt-für-Schritt-Anleitung zum Aufteilen von Videos
Speichern Sie diese vollständige Klasse als VideoSplitter.java. Das Ausgabeverzeichnis darf
noch nicht existieren, damit ein fehlgeschlagener Durchlauf kein früheres Ergebnis überschreiben kann.
Falls FFmpeg fehlschlägt, bleiben unvollständige Dateien zur Diagnose dort erhalten. Verwenden Sie das
Verzeichnis erst, nachdem diese Methode erfolgreich zurückgekehrt ist.
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.FFmpegFrameGrabber;
public final class VideoSplitter {
public static void split(Path input, Path output, double seconds) throws Exception {
if (!Double.isFinite(seconds) || seconds <= 0) {
throw new IllegalArgumentException("Segment duration must be positive and finite");
}
Path source = input.toRealPath();
if (!Files.isRegularFile(source)) throw new IOException("Input must be a regular file");
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(source.toFile())) {
grabber.start();
if (grabber.getImageWidth() <= 0) throw new IOException("Input has no video stream");
}
Files.createDirectory(output);
Path destination = output.toRealPath();
String executable = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
Process process = new ProcessBuilder(
executable, "-hide_banner", "-nostdin", "-v", "error", "-n",
"-i", source.toString(), "-map", "0:v:0", "-map", "0:a:0?",
"-c", "copy", "-f", "segment", "-segment_format", "mp4",
"-segment_time", BigDecimal.valueOf(seconds).toPlainString(), "-reset_timestamps", "1",
"segment-%03d.mp4"
).directory(destination.toFile()).redirectErrorStream(true)
.redirectOutput(destination.resolve("ffmpeg.log").toFile()).start();
try {
if (!process.waitFor(10, TimeUnit.MINUTES)) throw new IOException("FFmpeg timed out");
if (process.exitValue() != 0) throw new IOException("FFmpeg failed; inspect ffmpeg.log");
} finally {
if (process.isAlive()) {
process.destroyForcibly();
process.waitFor();
}
}
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("input output-directory seconds");
split(Path.of(args[0]), Path.of(args[1]), Double.parseDouble(args[2]));
}
}
Unter macOS oder Linux kompilieren Sie die Klasse und führen sie wie folgt aus:
mvn compile dependency:copy-dependencies
java -cp 'target/classes:target/dependency/*' VideoSplitter input.mp4 segments 10
FFmpeg beginnt ein Segment an einem Video-Keyframe an oder nach der angeforderten Grenze. Eine
angeforderte Dauer von 10 Sekunden ist ein Zielwert, keine exakte
Schnittvorgabe. Das letzte Segment kann kürzer sein; weit auseinanderliegende Keyframes können zu
wesentlich längeren Segmenten führen. Audiopakete behalten ihr relatives Timing bei; ihre Grenzen
müssen nicht exakt mit Videoframes zusammenfallen. Für exakte redaktionelle Schnittgrenzen ist ein
erneutes Encoding mit gezielt platzierten Keyframes erforderlich. Weitere Informationen finden Sie in
der Dokumentation zum Segment-Muxer.
Multithreading für mehr Leistung implementieren
Verarbeiten Sie den Paketstream jeder Eingabe sequenziell. Wenn Sie mehrere Decoder starten und
gemischte Audio- und Videoframes zählen, kann an den Grenzen von Positionssprüngen Audio verloren gehen
oder Inhalt dupliziert werden. Verwenden Sie für unabhängige Eingabedateien einen Executor mit
begrenzter Parallelität, geben Sie jedem Job ein eigenes Ausgabeverzeichnis und rufen Sie für jeden
übergebenen Job Future.get() auf, damit Fehler den Aufrufer erreichen. Messen Sie den
Durchsatz, bevor Sie die Parallelität erhöhen: Der I/O-Durchsatz des Speichersystems begrenzt häufig
das Kopieren von Paketen.
Tipps zur Leistungsoptimierung
Direktes Kopieren der Streams vermeidet Encoding-Aufwand und Qualitätsverluste durch erneutes Encoding. Dafür müssen die Codecs mit dem Ausgabecontainer kompatibel sein. Bei nicht unterstützten Codecs sollte ein sichtbarer Fehler auftreten, statt unbemerkt einen Stream zu verlieren. Verwenden Sie für Größenänderungen, Wasserzeichen oder Codec-Wechsel einen separaten Workflow zum Transkodieren und testen Sie dabei auch das Audio- und Timing-Verhalten.
Bewährte Verfahren zur Fehlerbehandlung
Das Beispiel weist nicht positive oder nicht endliche Zeitdauern, fehlende Eingaben und bereits
vorhandene Ausgabeverzeichnisse zurück. Zeitdauern werden als gebietsschemaunabhängige Dezimalzahlen
ohne Exponentialschreibweise angegeben, auch bei kleinen Werten wie
0.0001 Sekunden. Die Genauigkeits- und Bereichsgrenzen von FFmpeg für Zeitdauern
gelten weiterhin. Ein positiver, endlicher Wert des Java-Typs double allein
garantiert keine in FFmpeg darstellbare Zeitdauer. Fehler und Zeitüberschreitungen werden als
Fehlschläge weitergegeben.
Halten Sie ffmpeg.log privat, da dort Dateipfade enthalten sein können.
Eine Prozessgrenze ist keine Sandbox: Verarbeiten Sie nicht vertrauenswürdige Medien mit
Beschränkungen für Dateisystem, Netzwerk, CPU und Arbeitsspeicher, die Ihre Worker-Umgebung durchsetzt.
Praktische Tipps zur Optimierung der Videoverarbeitung
Prüfen Sie die tatsächlichen Ausgaben, auch für Eingaben mit Audio, mit variablen Bildraten und ohne Audio. Mit einer separat installierten FFmpeg-CLI:
ffprobe -v error -show_streams -show_format -of json segments/segment-000.mp4
ffmpeg -v error -i segments/segment-000.mp4 -f null -
Untersuchen Sie jedes Segment und vergleichen Sie die Gesamtzahl der Video- und Audiopakete sowie deren Nutzdaten mit der Eingabe. Berücksichtigen Sie Segmentlängen, die an Keyframes ausgerichtet sind, und Zeitstempelversätze des Containers. Allein die Existenz der Dateien belegt nicht, dass die Inhalte erhalten geblieben sind.
Fazit und weitere Ressourcen
Verwenden Sie JavaCV zur Untersuchung und den Paketsegmentierer von FFmpeg zum effizienten Aufteilen ohne erneutes Encoding. Informationen zum adaptiven Streaming finden Sie beim Service für Video-Encoding von Transloadit und beim 🤖 Robot /video/adaptive.
