Automate video highlights with Ruby and FFmpeg

Automating video editing tasks like creating highlight reels can significantly streamline your content creation workflow. Manually scrubbing through footage to find key moments is often tedious. In this DevTip, we'll explore how Ruby and FFmpeg can be combined to automatically generate engaging video highlights, saving you valuable time and enhancing viewer engagement.
Introduction to automated video highlights
Creating video highlights manually involves identifying significant moments, cutting them, and stitching them together. Automation helps by programmatically detecting potential highlights (like scene changes) and extracting short clips around those points. This makes your content more engaging by providing summaries or teasers, and makes your production workflow more efficient.
Setting up Ruby and FFmpeg
First, ensure you have Ruby and FFmpeg installed on your system.
Install FFmpeg:
# On macOS using homebrew
brew install ffmpeg
# On Ubuntu/Debian
sudo apt update && sudo apt install ffmpeg
Install Ruby:
If you don't have Ruby installed, you can use a version manager like rbenv
or rvm
, or install it
via your system's package manager.
# Example using homebrew on macOS
brew install ruby
Install the Ruby FFmpeg Wrapper:
We recommend using the instructure/ruby-ffmpeg
gem, which is actively maintained and supports
recent FFmpeg versions. Add it to your project's Gemfile
:
# Gemfile
gem 'ffmpeg', git: 'https://github.com/instructure/ruby-ffmpeg'
Then run bundle install
. Alternatively, install it directly:
gem install ffmpeg -g 'https://github.com/instructure/ruby-ffmpeg'
Verify Installations:
Check that both tools are correctly installed and accessible in your path:
ffmpeg -version
ruby -v
gem list ffmpeg
Version compatibility
Ensure you're using compatible versions for the instructure/ruby-ffmpeg
gem:
- Ruby: Version 3.1 or later
- FFmpeg: Versions 4, 5, 6, and 7 are supported
- Operating System: Cross-platform (Linux, macOS, Windows)
Extracting key moments using FFmpeg filters
FFmpeg provides powerful filters for analyzing video content. The select
filter combined with the
scene
score can detect significant changes between frames, which often correspond to potential
highlights. We can use this to output timestamps of these changes.
Here's an FFmpeg command to detect scenes with a change score greater than 0.3 (you can adjust this threshold) and print the presentation timestamp (PTS) of these frames to a metadata file:
ffmpeg -i input.mp4 \
-filter_complex "[0:v]select='gt(scene,0.3)',metadata=print:file=timestamps.txt" \
-an -f null -
This command analyzes input.mp4
, uses the select
filter to find frames where the scene change
score is above 0.3, and writes metadata (including timestamps) for these frames to timestamps.txt
.
We use -an -f null -
to discard the video output as we only need the metadata file.
Implementing Ruby scripts for automation
Now, let's create a Ruby class to encapsulate the logic for detecting scenes and creating highlight
clips using the ffmpeg
gem and the command above.
# Highlight_extractor.rb
require 'ffmpeg'
require 'fileutils'
require 'etc' # Required for Etc.nprocessors
require 'shellwords'
class HighlightExtractor
attr_reader :media, :output_dir
def initialize(input_file, output_dir = 'highlights')
raise "Input file not found: #{input_file}" unless File.exist?(input_file)
@media = FFMPEG::Media.new(input_file)
@output_dir = output_dir
FileUtils.mkdir_p(@output_dir) # Create output directory if it doesn't exist
end
# Detects scene changes and returns an array of timestamps (in seconds)
def detect_scene_changes(threshold = 0.3)
timestamp_file = File.join(@output_dir, "timestamps.txt")
# Configure FFmpeg global settings for performance
FFMPEG.threads = [Etc.nprocessors, 4].min # Use available processors, max 4
# Use Shellwords.escape for safety with file paths
escaped_input_path = Shellwords.escape(media.path)
escaped_timestamp_file = Shellwords.escape(timestamp_file)
command = %Q{ffmpeg -threads #{FFMPEG.threads} -i #{escaped_input_path} \
-filter_complex "[0:v]select='gt(scene,#{threshold})',metadata=print:file=#{escaped_timestamp_file}" \
-an -f null -}
puts "Running scene detection command..."
# puts command # Uncomment for debugging
system(command) # Execute the FFmpeg command
# Parse timestamps from the metadata file
timestamps = []
if File.exist?(timestamp_file)
File.readlines(timestamp_file).each do |line|
match = line.match(/pts_time:([\d.]+)/
timestamps << match[1].to_f if match
end
# FileUtils.rm(timestamp_file) # Optional: Clean up the timestamp file
else
puts "Warning: Timestamp file not found at #{timestamp_file}. Scene detection might have failed."
end
timestamps.uniq.sort # Return unique, sorted timestamps
end
# Creates short video clips around the detected timestamps
def create_highlight_clips(timestamps, clip_duration = 5, pre_roll = 2)
highlight_files = []
timestamps.each_with_index do |time, index|
start_time = [time - pre_roll, 0].max # Start 'pre_roll' seconds before the scene change, but not before 0
output_file = File.join(@output_dir, "highlight_#{index + 1}.mp4")
puts "Creating highlight #{index + 1} starting at #{start_time.round(2)}s..."
# Define preset for the highlight clip
preset = FFMPEG::Preset.new(name: 'highlight', filename: '%<basename>s.mp4') do
video_codec_name 'libx264' # Common H.264 codec
audio_codec_name 'aac' # Common AAC audio codec
seek_time start_time # Start time for the clip
duration clip_duration # Duration of the clip
# Add other options like bitrate, resolution if needed
# video_bitrate '1000k'
# resolution '1280x720'
end
transcoder = FFMPEG::Transcoder.new(
name: "highlight_#{index + 1}",
presets: [preset]
)
begin
# Configure timeout settings
FFMPEG.timeout = 300 # 5 minutes global timeout
FFMPEG.io_timeout = 30 # 30 seconds IO timeout
transcoder.process(media, output_file)
highlight_files << output_file
puts "Successfully created #{output_file}"
rescue FFMPEG::Error => e
puts "Error creating highlight #{index + 1} at #{time}s: #{e.message}"
rescue StandardError => e
puts "Unexpected error creating highlight #{index + 1}: #{e.message}"
end
end
highlight_files
end
end
Here's how you can use this class in a script:
#!/usr/bin/env ruby
# Generate_highlights.rb
require_relative 'highlight_extractor' # Assuming the class is in this file
require 'shellwords'
# --- helper functions ---
def filter_timestamps(timestamps, min_gap)
return [] if timestamps.empty?
filtered = [timestamps.first]
timestamps.drop(1).each do |time|
filtered << time if (time - filtered.last) >= min_gap
end
filtered
end
def create_highlight_compilation(highlight_files, output_file, temp_dir)
return if highlight_files.empty?
list_file = File.join(temp_dir, "filelist.txt")
# Ensure we use relative paths from the perspective of the temp_dir for concat
begin
File.open(list_file, 'w') do |f|
highlight_files.each do |file|
# Use relative path from the output directory for the list file
relative_path = File.basename(file)
f.puts "file '#{relative_path.gsub("'", "'\\\\'"')}'" # Escape single quotes
end
end
# Concatenate all highlights using the concat demuxer
# Use -safe 0 as we are controlling the paths in filelist.txt
# Use -c copy for speed (no re-encoding), requires clips to have compatible streams
escaped_list_file = Shellwords.escape(list_file)
escaped_output_file = Shellwords.escape(output_file)
# Run ffmpeg from the output directory to resolve relative paths easily
concat_command = %Q{ffmpeg -f concat -safe 0 -i #{escaped_list_file} -c copy #{escaped_output_file}}
puts "\nRunning concatenation command in dir '#{temp_dir}'..."
# puts concat_command # Uncomment for debugging
system(concat_command, chdir: temp_dir) # Run ffmpeg in the output directory
if $?.success?
puts "Successfully created compilation: #{output_file}"
else
puts "Error creating compilation file."
end
ensure
# FileUtils.rm(list_file) if File.exist?(list_file) # Optional: Clean up the list file
end
end
# --- configuration ---
input_file = ARGV[0]
output_dir = ARGV[1] || 'highlights'
scene_threshold = 0.3 # Sensitivity for scene detection (0.0 to 1.0)
clip_duration = 5 # Duration of each highlight clip in seconds
pre_roll = 2 # Seconds before the detected scene change to start the clip
min_gap_between_highlights = 10 # Minimum seconds between highlights
# --- main execution ---
if input_file.nil?
puts "Usage: ruby generate_highlights.rb <input_video.mp4> [output_directory]"
exit(1)
end
unless File.exist?(input_file)
puts "Error: Input file '#{input_file}' not found."
exit(1)
end
begin
extractor = HighlightExtractor.new(input_file, output_dir)
puts "Detecting scene changes in '#{input_file}' (threshold: #{scene_threshold})..."
timestamps = extractor.detect_scene_changes(scene_threshold)
if timestamps.empty?
puts "No significant scene changes detected."
else
puts "Found #{timestamps.size} potential highlight timestamps."
# Optional: Filter timestamps that are too close together
filtered_timestamps = filter_timestamps(timestamps, min_gap_between_highlights)
if timestamps.size != filtered_timestamps.size
puts "Filtered down to #{filtered_timestamps.size} highlights (min gap: #{min_gap_between_highlights}s)."
end
if filtered_timestamps.empty?
puts "No highlights remaining after filtering."
else
puts "\nCreating #{filtered_timestamps.size} highlight clips (duration: #{clip_duration}s, pre-roll: #{pre_roll}s)..."
highlight_files = extractor.create_highlight_clips(filtered_timestamps, clip_duration, pre_roll)
if highlight_files.empty?
puts "\nNo highlight clips were successfully generated."
else
puts "\nSuccessfully created #{highlight_files.size} highlight clips in '#{output_dir}/'."
# Optional: Create a compilation reel
compilation_file = File.join(output_dir, "highlight_reel.mp4")
create_highlight_compilation(highlight_files, compilation_file, output_dir)
end
end
end
rescue FFMPEG::Error => e
puts "\nFFmpeg Error: #{e.message}"
exit(1)
rescue StandardError => e
puts "\nAn unexpected error occurred: #{e.message}"
puts e.backtrace.join("\n")
exit(1)
end
puts "\nHighlight generation process finished."
Save the class as highlight_extractor.rb
and the script as generate_highlights.rb
. Run it from
your terminal like this:
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Performance optimization
Processing video can be resource-intensive. Here are ways to optimize performance:
-
Threading: Use multiple CPU cores during FFmpeg operations. The
instructure/ruby-ffmpeg
gem allows global configuration:require 'etc' # Use up to 4 cores, or fewer if the system has less FFMPEG.threads = [Etc.nprocessors, 4].min
-
Timeouts: Prevent FFmpeg processes from running indefinitely if they encounter issues:
FFMPEG.timeout = 300 # Set global timeout to 5 minutes FFMPEG.io_timeout = 30 # Set IO timeout to 30 seconds
-
Hardware Acceleration: For significant speedups on supported hardware (like NVIDIA GPUs with NVENC or macOS with VideoToolbox), explore FFmpeg's hardware acceleration options (e.g.,
-hwaccel cuda
,-c:v h264_nvenc
). Integrating these often requires specific FFmpeg builds and adds complexity to your commands and presets.
Optimizing highlight selection
- Threshold Adjustment: The
scene
threshold (e.g.,0.3
) is crucial. A lower value detects more changes (potentially noise), while a higher value detects only major changes. Experiment to find what works best for your content type. - Filtering Logic: The
filter_timestamps
function in the example script prevents creating highlights too close together. You could enhance this logic, perhaps by considering the duration between scene changes or analyzing the magnitude of the scene change score.
Practical examples and use cases
This automated approach is valuable in various scenarios:
- Sports: Generate highlight reels from game footage by detecting goals, scores, or significant plays (scene changes often accompany these).
- Webinars/Talks: Extract key moments or slide transitions for promotional clips.
- Gaming Streams: Automatically capture exciting moments like kills, wins, or major events.
- Interviews: Identify segments where speakers change or key points are made.
- Vlogs: Quickly create montages or summaries from longer footage.
- Security Footage: Isolate segments with significant motion or changes.
The compilation example (create_highlight_compilation
) shows how to easily stitch the generated
clips into a single highlight reel using FFmpeg's concat
demuxer.
Conclusion and potential enhancements
Combining Ruby's scripting capabilities with FFmpeg's powerful media processing tools provides an effective way to automate video highlight generation, significantly boosting content creation efficiency.
Potential enhancements include:
- Advanced Scene Detection: Explore other FFmpeg filters (like
blackdetect
orsilencedetect
) or combinations for more nuanced detection. - Audio Analysis: Incorporate audio analysis (e.g., detecting loud sounds, specific keywords, or changes in volume) to identify highlights based on sound events.
- Machine Learning: Integrate ML models for more sophisticated content analysis (e.g., object recognition, action detection) to identify specific types of highlights relevant to your domain.
- Web Interface: Build a simple web UI (e.g., using Sinatra or Rails) to allow users to upload videos and manage the highlight generation process.
For robust, scalable video processing in production environments, consider cloud-based services. Transloadit leverages FFmpeg extensively in its video encoding service, providing managed infrastructure and pre-built Robots that simplify complex tasks like automated highlight generation within your application workflows.