Videos in Scala mit FFmpeg verketten
Das Verketten von Videos ist eine gängige Anforderung in Anwendungen zur Medienverarbeitung, ob Sie nun einen Videoeditor entwickeln, Zusammenschnitte erstellen oder Video-Workflows automatisieren. In diesem Leitfaden zeigen wir, wie Sie Videos mit Scala und FFmpeg effizient verketten, und gehen dabei auf verschiedene Methoden und bewährte Verfahren ein.
Umgebung einrichten
Scala installieren
Der empfohlene Weg, Scala zu installieren, führt über Coursier (cs setup):
macOS:
brew install coursier/formulas/coursier && cs setup
Linux:
curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs && chmod +x cs && ./cs setup
Windows: Laden Sie den Scala-Installer für Windows herunter und führen Sie ihn aus
FFmpeg installieren
macOS:
brew install ffmpeg
Linux (Ubuntu/Debian):
sudo apt update && sudo apt install ffmpeg
Windows: Laden Sie es von der offiziellen FFmpeg-Website herunter
Methoden zur Videoverkettung
FFmpeg bietet zwei Hauptmethoden, um Videos zu verketten:
- Concat-Demuxer: ideal für Videos mit gleichem Codec und Format
- Concat-Filter: flexibler, erfordert aber ein erneutes Encoding
Implementieren wir beide Methoden in Scala:
import scala.sys.process.*
import scala.util.{Try, Success, Failure}
import java.io.{File, PrintWriter}
import java.nio.file.{Files, Paths}
enum ConcatMethod:
case Demuxer, Filter
case class VideoFile(path: String):
def exists: Boolean = Files.exists(Paths.get(path))
def extension: String = path.split("\\.").lastOption.getOrElse("")
object VideoConcatenator:
def createConcatFile(videos: Seq[VideoFile], tempFile: String): Try[Unit] = Try {
val writer = PrintWriter(File(tempFile))
try
videos.foreach { video =>
val path = Paths.get(video.path).toAbsolutePath.toString
require(!path.contains('\n') && !path.contains('\r'), "Paths must not contain newlines")
val escaped = path.replace("'", "'\\''")
writer.println(s"file '$escaped'")
}
finally
writer.close()
}
def concatenateVideos(
videos: Seq[VideoFile],
output: String,
method: ConcatMethod = ConcatMethod.Demuxer
): Try[Boolean] = Try {
if videos.isEmpty then throw IllegalArgumentException("No videos provided")
if !videos.forall(_.exists) then throw IllegalArgumentException("One or more input videos not found")
method match
case ConcatMethod.Demuxer =>
val tempFile = Files.createTempFile("video-concat-", ".txt").toString
createConcatFile(videos, tempFile).get
val cmd = Seq(
"ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", tempFile,
"-c", "copy",
"-y",
output
)
try cmd.! == 0
finally Files.deleteIfExists(Paths.get(tempFile))
case ConcatMethod.Filter =>
val inputs = videos.flatMap(v => Seq("-i", v.path))
val inputsForFilter = videos.indices.map(i => s"[$i:v:0][$i:a:0]").mkString
val filter = s"${inputsForFilter}concat=n=${videos.length}:v=1:a=1[outv][outa]"
val cmd = Seq("ffmpeg") ++ inputs ++ Seq(
"-filter_complex", filter,
"-map", "[outv]",
"-map", "[outa]",
"-y",
output
)
cmd.! == 0
}
@main def run(args: String*): Unit =
if args.length < 3 then
println("Usage: VideoConcatenator <output_file> <input_file1> <input_file2> [input_file3...] [--filter]")
sys.exit(1)
val useFilter = args.contains("--filter")
val output = args(0)
val videos = args.slice(1, args.length).filterNot(_ == "--filter").map(VideoFile.apply)
val method = if useFilter then ConcatMethod.Filter else ConcatMethod.Demuxer
concatenateVideos(videos, output, method) match
case Success(true) => println(s"Successfully concatenated videos to $output")
case Success(false) => println("Failed to concatenate videos: FFmpeg returned non-zero exit code")
case Failure(e) => println(s"Error concatenating videos: ${e.getMessage}")
Die Methoden verstehen
Concat-Demuxer
Der Concat-Demuxer ist schneller, da er kein erneutes Encoding erfordert. Er funktioniert so:
- Eine Textdatei erstellen, die die Eingabevideos auflistet
- Sie mit dem Concat-Demuxer von FFmpeg zusammenführen
- Die Streams direkt in die Ausgabe kopieren
Diese Methode ist ideal, wenn Ihre Videos denselben Codec und dasselbe Format verwenden.
Concat-Filter
Der Concat-Filter ist flexibler, aber langsamer, da er ein erneutes Encoding erfordert. Verwenden Sie diese Methode, wenn:
- Videos unterschiedliche Codecs haben
- Sie Übergänge hinzufügen müssen
- Sie nach dem Normalisieren der Videoabmessungen mit unterschiedlichen Codecs arbeiten
Dieses Filterbeispiel erfordert einen Videostream und einen Audiostream pro Eingabe, übereinstimmende Videoabmessungen sowie Zeitstempel, die bei null beginnen. Normalisieren Sie diese Eigenschaften zuerst. Übergänge fügt es nicht hinzu.
Fehlerbehandlung und Optimierung
Unsere Implementierung enthält mehrere Funktionen zur Fehlerbehandlung:
- Validierung der Eingaben
- Prüfungen, ob die Dateien vorhanden sind
- Ordnungsgemäßes Aufräumen von Ressourcen
- Detaillierte Fehlerberichte
Für bessere Performance:
- Verwenden Sie nach Möglichkeit die Demuxer-Methode
- Stellen Sie sicher, dass die Eingabevideos im selben Format vorliegen
- Ziehen Sie Hardwarebeschleunigung mit
-hwaccelin Betracht - Räumen Sie temporäre Dateien auf
Fortgeschrittene Techniken
Übergänge hinzufügen
Das folgende Beispiel arbeitet ausschließlich mit Video und blendet genau zwei Clips mit
übereinstimmenden Abmessungen und übereinstimmender Bildrate ineinander über. Geben Sie die Dauer
des ersten Clips in Sekunden an; Audio wird bewusst mit -an weggelassen.
def concatenateWithTransitions(
first: VideoFile,
second: VideoFile,
firstDuration: Double,
output: String,
transitionDuration: Double = 1.0
): Try[Boolean] = Try {
require(transitionDuration > 0 && firstDuration > transitionDuration)
val offset = firstDuration - transitionDuration
val filter = "[0:v]settb=AVTB,setpts=PTS-STARTPTS[v0];" +
"[1:v]settb=AVTB,setpts=PTS-STARTPTS[v1];" +
s"[v0][v1]xfade=transition=fade:duration=$transitionDuration:offset=$offset[outv]"
val cmd = Seq("ffmpeg", "-i", first.path, "-i", second.path,
"-filter_complex", filter,
"-map", "[outv]", "-an",
"-y",
output
)
cmd.! == 0
}
Unterschiedliche Formate verarbeiten
Dieses Beispiel normalisiert Codecs, eine Fläche von 1280 mal 720, Bildrate, Zeitstempel und Audioformat. Jede Eingabe muss einen Audiostream enthalten; fügen Sie für Clips ohne Audio separat Stille hinzu.
def normalizeVideo(input: VideoFile, output: String): Try[Boolean] = Try {
val cmd = Seq(
"ffmpeg",
"-i", input.path,
"-vf", "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30,setpts=PTS-STARTPTS",
"-af", "asetpts=PTS-STARTPTS", "-ar", "48000", "-ac", "2",
"-c:v", "libx264",
"-c:a", "aac",
"-y",
output
)
cmd.! == 0
}
Fazit
Das Verketten von Videos in Scala mit FFmpeg bietet eine leistungsfähige Möglichkeit, Videodateien programmatisch zusammenzuführen. Die Wahl zwischen Concat-Demuxer und Concat-Filter hängt von Ihren konkreten Anforderungen an Geschwindigkeit gegenüber Flexibilität ab.
Für fortgeschrittenere Funktionen zur Videoverarbeitung, einschließlich automatisierter Videoverkettung im großen Maßstab, werfen Sie einen Blick auf den Video-Encoding-Service von Transloadit.
