Effiziente Videoteilung in Java mit FFmpeg und JavaCV
Beim Teilen eines Videos müssen Audio und Präsentations-Timing erhalten bleiben. Die Anzahl der
Aufrufe von FFmpegFrameGrabber.grab() misst nicht die Videodauer: Ein Aufruf kann einen Audioblock
oder einen Videoframe zurückgeben. Auch variable Bildraten machen Berechnungen anhand der Frame-Anzahl
unzuverlässig.
Dieses Beispiel nutzt JavaCV, um die Eingabe zu untersuchen, und das mitgelieferte ausführbare FFmpeg-Programm, um komprimierte Pakete aufzuteilen. FFmpeg übernimmt die Zeitstempel und das Audio-Interleaving. Keine Shell interpretiert den Eingabedateinamen.
Warum Videos teilen?
Segmente eignen sich für die Bearbeitung, die Bereitstellung von Downloads und die unabhängige Verarbeitung. Einzelne MP4-Segmente bilden noch keine adaptive Streaming-Präsentation; HLS oder DASH benötigt zusätzlich ein Manifest und entsprechend codierte Qualitätsstufen.
Ihre Java-Umgebung einrichten
Verwenden Sie für die getestete Linux-Umgebung JDK 21.0.12.1 und Maven
3.9.16. Speichern Sie dies als pom.xml und legen Sie
anschließend die unten gezeigte Datei VideoSplitter.java in
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, zum Beispiel
linux-x86_64 statt macosx-arm64. So werden nur die benötigten nativen
Artefakte heruntergeladen. Unterstützte 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 ist kein Stream-Copying.
Zum Teilen ohne visuelle Bearbeitung nutzt der folgende Unterprozess den Paketpfad
-c copy von FFmpeg. Er erhält den ersten Videostream und, sofern vorhanden, den
ersten Audiostream. Zusätzliche Audiospuren, Untertitel, Kapitel, Rotationsmetadaten und beliebige
Container-Metadaten werden in diesem Beispiel nicht ausdrücklich erhalten. Prüfen Sie diese separat,
bevor Sie die Stream-Zuordnung erweitern.
Schritt-für-Schritt-Anleitung zum Teilen 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.
Wenn FFmpeg fehlschlägt, bleiben dort unvollständige Dateien zur Diagnose erhalten. Verwenden Sie
intakte Eingaben und validieren Sie die Segmente wie unten beschrieben, bevor Sie das
Ausgabeverzeichnis nutzen. Der Grabber prüft Metadaten; beim Kopieren von Paketen werden die Medien
nicht vollständig decodiert, um sie zu validieren.
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", "-xerror", "-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 und starten Sie die Klasse mit:
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, kein exakter Schnitt.
Das letzte Segment kann kürzer sein, und weit auseinanderliegende Keyframes können deutlich längere
Segmente erzeugen. Audiopakete behalten ihr relatives Timing; ihre Grenzen müssen nicht exakt mit
Videoframes zusammenfallen. Für exakte Schnittgrenzen ist erneutes Encoding mit gezielt platzierten
Keyframes erforderlich. Siehe die
Dokumentation zum Segment-Muxer.
Multithreading für höhere Leistung implementieren
Verarbeiten Sie den Paketstream jeder Eingabe sequenziell. Mehrere Decoder zu starten und gemischte
Audio-/Videoframes zu zählen, kann dazu führen, dass Audio verloren geht oder Inhalte an
Sprunggrenzen 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
Future.get() für jeden übergebenen Job auf, damit Fehler den Aufrufer erreichen.
Messen Sie den Durchsatz, bevor Sie die Parallelität erhöhen: Beim Kopieren von Paketen ist oft die
Lese- und Schreibgeschwindigkeit des Speichermediums der begrenzende Faktor.
Tipps zur Leistungsoptimierung
Stream-Copying vermeidet Encoding-Aufwand und Generationsverluste. Es erfordert Codecs, die mit dem Ausgabecontainer kompatibel sind; bei nicht unterstützten Codecs sollte ein sichtbarer Fehler auftreten, statt unbemerkt einen Stream zu verlieren. Nutzen 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 lehnt nicht positive oder nicht endliche Zeitdauern, fehlende Eingaben und bereits
vorhandene Ausgabeverzeichnisse ab. Zeitdauern verwenden einfache Dezimalzahlen in einem
locale-unabhängigen Format, auch für kleine Werte wie 0.0001 Sekunden.
Die Genauigkeit und der Wertebereich 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.
Die Option -xerror von FFmpeg stoppt die
Verarbeitung bei erkannten Fehlern, einschließlich beschädigter Pakete, die der Demuxer meldet.
Auch bei erfolgreicher Rückgabe müssen Sie die Ausgaben prüfen: Stream-Copying kann Beschädigungen
weiterreichen, die nur ein Decoder erkennt.
Halten Sie ffmpeg.log vertraulich, da die Logdatei Dateipfade enthalten kann.
Eine Prozessgrenze ist keine Sandbox: Verarbeiten Sie nicht vertrauenswürdige Medien nur mit
Beschränkungen für Dateisystem, Netzwerk, CPU und Arbeitsspeicher, die Ihre Worker-Umgebung durchsetzt.
Praxistipps zur Optimierung der Videoverarbeitung
Untersuchen Sie die tatsächlichen Ausgaben, auch für Eingaben mit Audio, variablen Bildraten und ohne
Audio. Führen Sie nach separater Installation von ffmpeg und
ffprobe Folgendes im Projektverzeichnis aus, um jedes Segment zu untersuchen und
zu decodieren. Die Subshell stoppt bei der ersten fehlgeschlagenen Prüfung:
(
set -e
for segment in segments/segment-*.mp4; do
ffprobe -v error -show_streams -show_format -of json "$segment"
ffmpeg -v error -xerror -err_detect explode -i "$segment" \
-map 0:v:0 -map '0:a:0?' -f null -
done
)
Prüfen Sie die Diagnosemeldungen auch dann, wenn ein Befehl erfolgreich ist. Vergleichen Sie die Gesamtzahl der Video-/Audiopakete und ihre Nutzdaten mit der Eingabe und gleichen Sie das erwartete Ende mit einer nachweislich vollständigen Quelle ab. Berücksichtigen Sie Segmentlängen, die an Keyframes ausgerichtet sind, und Zeitstempel-Offsets des Containers; allein die Existenz von Dateien belegt nicht, dass die Inhalte erhalten geblieben sind.
Fazit und weitere Ressourcen
Nutzen Sie JavaCV zur Untersuchung und den Paketsegmentierer von FFmpeg für effizientes Teilen ohne erneutes Encoding. Informationen zu adaptivem Streaming finden Sie beim Video-Encoding-Service von Transloadit und beim Robot 🤖 /video/adaptive.
