Add subtitles to videos in Ruby with open-source tools
Adding subtitles to videos significantly enhances accessibility, viewer engagement, and SEO. Ruby developers have access to several powerful open-source tools that simplify subtitle integration and editing. Let's explore how you can leverage these tools effectively.
Why subtitles matter
Subtitles aren't just beneficial for viewers with hearing impairments—they also improve comprehension for non-native speakers, enhance SEO by making content searchable, and can increase viewer retention. Providing subtitles is becoming a standard practice for inclusive and accessible content.
Open-source Ruby tools for subtitles
Ruby offers several robust libraries for subtitle management, primarily by interfacing with the powerful FFmpeg multimedia framework or by directly manipulating subtitle files:
- streamio-ffmpeg: A comprehensive Ruby wrapper for FFmpeg that allows various video processing tasks, including burning subtitles into videos or converting subtitle formats.
- srt: A dedicated gem for parsing, manipulating, and generating SRT (SubRip Text) subtitle files, the most common subtitle format.
Let's dive into practical examples using these tools.
Adding subtitles with streamio-ffmpeg
The streamio-ffmpeg gem provides a convenient Ruby interface to FFmpeg, making it easy to embed
subtitles into videos. This process is often called "burning" subtitles, meaning they become part of
the video frames.
Use a maintained Ruby release and an FFmpeg build with libass's subtitles filter, the libx264
encoder, and FFprobe. Verify the filter with ffmpeg -filters; it is not included in every package
build. Install these tested gem versions:
gem install streamio-ffmpeg:3.0.2 srt:0.1.5
Save this shared implementation as subtitle_tools.rb. All examples below reuse it. The temporary
directory keeps the subtitle filename out of FFmpeg filter syntax and isolates the wrapper's
overwrite behavior from existing outputs. Publication uses a hard link on the output filesystem;
the filesystem must support hard links.
require 'streamio-ffmpeg'
require 'srt'
require 'tmpdir'
module SubtitleTools
def self.load_srt(path)
raise ArgumentError, 'Subtitle input must be a regular file' unless File.file?(path)
text = File.binread(path, 1024 * 1024 + 1)
raise ArgumentError, 'Subtitle input exceeds 1 MiB' if text.bytesize > 1024 * 1024
text.force_encoding(Encoding::UTF_8)
raise ArgumentError, 'Subtitles must be valid UTF-8' unless text.valid_encoding?
subtitles = SRT::File.parse(text)
if subtitles.lines.empty? || !subtitles.errors.empty?
raise ArgumentError, 'Invalid or empty SRT input'
end
unless subtitles.lines.all? { |line| line.start_time >= 0 && line.end_time > line.start_time }
raise ArgumentError, 'Invalid subtitle timing'
end
subtitles
end
def self.with_output(output_path, extension)
output = File.expand_path(output_path)
if File.exist?(output) || File.symlink?(output)
raise ArgumentError, 'Output must not exist'
end
Dir.mktmpdir('.subtitles-', File.dirname(output)) do |temporary|
candidate = File.join(temporary, "result#{extension}")
yield candidate, temporary
raise IOError, 'No nonempty output produced' unless File.file?(candidate) && File.size(candidate) > 0
File.link(candidate, output)
end
end
def self.burn(video_path, subtitle_path, output_path)
subtitles = load_srt(subtitle_path)
movie = FFMPEG::Movie.new(File.expand_path(video_path))
raise ArgumentError, 'Invalid video input' unless movie.valid?
with_output(output_path, '.mp4') do |_candidate, temporary|
File.write(File.join(temporary, 'captions.srt'), subtitles.to_s)
# Fixed basenames avoid FFmpeg's separate filter-path escaping rules.
Dir.chdir(temporary) do
movie.transcode('result.mp4', {
video_codec: 'libx264', audio_codec: 'aac',
custom: ['-nostdin', '-vf', 'subtitles=captions.srt', '-threads', '2']
})
end
end
end
def self.write_srt(subtitles, output_path)
with_output(output_path, '.srt') { |candidate| File.write(candidate, subtitles.to_s) }
end
def self.convert_to_vtt(subtitle_path, output_path)
subtitles = load_srt(subtitle_path)
with_output(output_path, '.vtt') do |candidate, temporary|
input = File.join(temporary, 'captions.srt')
File.write(input, subtitles.to_s)
unless system('ffmpeg', '-nostdin', '-n', '-i', input, '-c:s', 'webvtt', candidate)
raise IOError, 'Subtitle conversion failed'
end
end
end
end
This implementation is for a sequential CLI worker: Dir.chdir affects the whole Ruby process,
so do not call it concurrently from multiple threads. Use separate isolated processes when scaling
up. FFmpeg and the parser must still run with resource limits for untrusted files; filename safety
does not make a media-processing sandbox. The helper re-encodes video and audio into MP4.
Use it from a script in the same directory:
require_relative 'subtitle_tools'
SubtitleTools.burn('input.mp4', 'subtitles.srt', 'output_with_subs.mp4')
puts 'Subtitles added'
Manipulating SRT files with the SRT gem
The srt gem allows you to programmatically parse, modify, and create SRT subtitle files. This is
useful for tasks like adjusting timing, correcting text, or generating subtitles from scratch.
require_relative 'subtitle_tools'
subtitles = SubtitleTools.load_srt('subtitles.srt')
subtitles.timeshift(all: '+2.5s')
SubtitleTools.write_srt(subtitles, 'modified.srt')
puts 'Subtitles shifted by 2.5 seconds'
The gem's timeshift method takes an options hash, not a numeric delay. Its parser can record errors
on subtitle lines rather than raising a dedicated SRT::Error; the shared loader checks those
reported errors and validates positive cue durations before processing.
Converting subtitle formats
While the srt gem focuses specifically on the SRT format, you can leverage FFmpeg through the
streamio-ffmpeg gem to convert between various subtitle formats. This is particularly useful for
web-based video players that often prefer the WebVTT format.
require_relative 'subtitle_tools'
SubtitleTools.convert_to_vtt('subtitles.srt', 'subtitles.vtt')
puts 'WebVTT subtitles saved'
The helper executes FFmpeg with separate arguments, checks its exit status, and publishes only a nonempty result. Keep format and extension choices together when adapting it to another codec.
Error handling for subtitle integration
Let helper failures propagate to one CLI boundary. Save this as add_subtitles.rb beside
subtitle_tools.rb; it returns a nonzero status without exposing raw decoder or document errors.
require_relative 'subtitle_tools'
abort 'Usage: ruby add_subtitles.rb <video.mp4> <subtitles.srt> <new-output.mp4>' unless ARGV.length == 3
begin
SubtitleTools.burn(*ARGV)
puts 'Subtitles added'
rescue StandardError
warn 'Subtitle processing failed. Check the inputs, output path, and FFmpeg installation.'
exit 1
end
Run ruby add_subtitles.rb input.mp4 subtitles.srt output_with_subs.mp4. Decoder diagnostics may
still appear in a local FFmpeg log; do not send those logs directly to web clients.
Supported subtitle formats
When working with subtitles, especially with FFmpeg, you might encounter various formats. Key formats include:
- SRT (SubRip Text):
.srt- The most widely supported plain-text format. Handled directly by thesrtgem and FFmpeg. - WebVTT (Web Video Text Tracks):
.vtt- The standard for HTML5 video subtitles, supporting styling and positioning. FFmpeg can convert to/from this format. - ASS/SSA (Advanced SubStation Alpha / SubStation Alpha):
.ass/.ssa- More advanced formats supporting complex styling, positioning, and effects, often used in anime fansubs. FFmpeg has good support. - SBV (SubViewer):
.sbv- A simple format used by YouTube. FFmpeg can handle conversion.
FFmpeg provides broad support for converting between these and other formats. The srt gem
specifically focuses on parsing and manipulating the SRT format.
Practical example: batch processing videos with subtitles
Here's a script demonstrating how to process multiple videos in a directory, adding corresponding SRT subtitles if found.
require_relative 'subtitle_tools'
def batch_process_videos(video_dir, subtitle_dir, output_dir)
video_files = Dir.children(video_dir).sort.filter_map do |name|
path = File.join(video_dir, name)
path if File.extname(name).downcase == '.mp4' && File.file?(path) && !File.symlink?(path)
end
raise ArgumentError, 'No MP4 files found' if video_files.empty?
Dir.mkdir(output_dir, 0700)
failures = 0
video_files.each do |video|
basename = File.basename(video, File.extname(video))
begin
SubtitleTools.burn(video, File.join(subtitle_dir, "#{basename}.srt"),
File.join(output_dir, "#{basename}_subtitled.mp4"))
puts "Converted #{basename}"
rescue StandardError
failures += 1
warn "Conversion failed for #{basename}"
end
end
failures.zero? ? 0 : 1
end
abort 'Usage: ruby batch_subtitles.rb <videos> <subtitles> <new-output-directory>' unless ARGV.length == 3
begin
exit batch_process_videos(*ARGV)
rescue StandardError
warn 'Cannot prepare batch. Check input directories and use a new output directory.'
exit 1
end
This script iterates through MP4 files in a specified directory, looks for a matching SRT file in
another directory, and uses SubtitleTools.burn to process each pair, placing the
output in a designated folder. Save it as batch_subtitles.rb. Missing or invalid subtitles count
as failures, but remaining files are still attempted. Completed outputs are retained and partial
temporary files are cleaned up. The overall exit status reports whether every conversion succeeded.
Conclusion
Adding subtitles to videos using Ruby is straightforward with open-source tools like
streamio-ffmpeg (interfacing with FFmpeg) and the srt gem. These libraries simplify burning
subtitles into videos, manipulating SRT files, and converting between formats, significantly
enhancing your video's accessibility and viewer engagement. Remember to handle potential errors
gracefully for robust applications.
For more complex subtitle workflows, large-scale video processing, or cloud-based solutions, consider a dedicated service. Transloadit's 🤖 /video/subtitle Robot offers advanced subtitle integration capabilities as part of its Video Encoding service, handling various formats and styles efficiently.
