Concatenate videos in Scala with FFmpeg

Video concatenation is a common requirement in media processing applications, whether you're building a video editor, creating compilations, or automating video workflows. In this guide, we'll explore how to concatenate videos efficiently using Scala and FFmpeg, covering different methods and best practices.
Set up your environment
Install Scala
The recommended way to install Scala is using 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: Download and execute the Scala installer for Windows
Install FFmpeg
macOS:
brew install ffmpeg
Linux (Ubuntu/Debian):
sudo apt update && sudo apt install ffmpeg
Windows: Download from the official FFmpeg website
Video concatenation methods
FFmpeg offers two main methods for concatenating videos:
- Concat demuxer: Ideal for videos with the same codec and format
- Concat filter: More flexible but requires re-encoding
Let's implement both methods 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 => writer.println(s"file '${video.path}'"))
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 = "concat_list.txt"
createConcatFile(videos, tempFile).get
val cmd = Seq(
"ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", tempFile,
"-c", "copy",
"-y",
output
)
val result = cmd.! == 0
Files.deleteIfExists(Paths.get(tempFile))
result
case ConcatMethod.Filter =>
val inputs = videos.flatMap(v => Seq("-i", v.path))
val filter = s"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}")
Understanding the methods
Concat demuxer
The concat demuxer is faster as it doesn't require re-encoding. It works by:
- Creating a text file listing input videos
- Using FFmpeg's concat demuxer to combine them
- Copying streams directly to the output
This method is ideal when your videos share the same codec and format.
Concat filter
The concat filter is more flexible but slower as it requires re-encoding. Use this method when:
- Videos have different codecs
- You need to add transitions
- You're working with different formats or resolutions
Error handling and optimization
Our implementation includes several error handling features:
- Input validation
- File existence checks
- Proper resource cleanup
- Detailed error reporting
For better performance:
- Use the demuxer method when possible
- Ensure input videos are in the same format
- Consider using hardware acceleration with
-hwaccel
- Clean up temporary files
Advanced techniques
Adding transitions
To add crossfade transitions between videos:
def concatenateWithTransitions(
videos: Seq[VideoFile],
output: String,
transitionDuration: Double = 1.0
): Try[Boolean] = Try {
val inputs = videos.flatMap(v => Seq("-i", v.path))
val filters = videos.indices.map(i =>
if i == 0 then s"[$i:v]setpts=PTS-STARTPTS[v$i]"
else s"[$i:v]setpts=PTS-STARTPTS[v$i];[v${i-1}][v$i]xfade=transition=fade:duration=$transitionDuration:offset=${i*5}[v${i+1}]"
)
val cmd = Seq("ffmpeg") ++ inputs ++ Seq(
"-filter_complex", filters.mkString(";"),
"-map", s"[v${videos.length}]",
"-y",
output
)
cmd.! == 0
}
Handling different formats
When working with different formats, you might need to normalize them first:
def normalizeVideo(input: VideoFile, output: String): Try[Boolean] = Try {
val cmd = Seq(
"ffmpeg",
"-i", input.path,
"-c:v", "libx264",
"-c:a", "aac",
"-y",
output
)
cmd.! == 0
}
Conclusion
Video concatenation in Scala using FFmpeg offers a powerful way to combine video files programmatically. The choice between the concat demuxer and filter methods depends on your specific requirements for speed versus flexibility.
For more advanced video processing capabilities, including automated video concatenation at scale, check out Transloadit's Video Encoding service.