Video-Highlights mit Ruby und FFmpeg automatisieren
Interessante Momente in langen Aufnahmen zu finden, kostet Zeit. Ruby kann Szenenwechsel automatisch erkennen, Clip-Kandidaten extrahieren und mit FFmpeg zu einem Sichtungsvideo zusammenfügen. Diese Heuristik erkennt visuelle Änderungen, versteht aber nicht, welche Momente wichtig sind.
Einführung in automatisierte Video-Highlights
Der Workflow erkennt große Änderungen zwischen benachbarten Frames, wählt Zeitstempel mit genügend Abstand aus und codiert kurze Clips um diese Zeitpunkte. Ein Schnitt, Blitz oder Folienwechsel kann ein nützlicher Kandidat sein, doch ein Tor oder eine wichtige Aussage muss keinen Szenenschnitt enthalten. Sichten Sie das erzeugte Video vor der Veröffentlichung.
Ruby und FFmpeg einrichten
Verwenden Sie Bash unter Linux mit Ruby, FFmpeg und dem zugehörigen ffprobe auf PATH.
Diese Anleitung wurde mit Ruby 3.4.10 und FFmpeg/ffprobe 9.0.1 getestet. Das sind getestete Versionen,
keine exakten Installationsanforderungen. Verwenden Sie eine aktuelle Patch-Version von Ruby 3.4 aus
der Ruby-Installationsanleitung und prüfen Sie den
Wartungsstatus.
Installieren Sie unter Ubuntu/Debian FFmpeg und ffprobe zusammen:
sudo apt-get update && sudo apt-get install -y --no-install-recommends ffmpeg
Das Beispiel nutzt die Standardbibliotheken von Ruby ohne FFmpeg-Wrapper von Drittanbietern. Prüfen Sie vor dem Speichern der Skripte, ob Ruby die Bibliotheken laden kann, und sehen Sie sich die Filter und Encoder Ihres FFmpeg-Builds an:
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE ruby -v &&
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE ruby -e "require 'fileutils'; require 'json'; require 'open3'" &&
ffmpeg -version && ffprobe -version && ffmpeg -filters && ffmpeg -encoders
Versionskompatibilität
Ihr FFmpeg-Build benötigt die Filter select, metadata, atrim,
asetpts und concat sowie die Encoder libx264 und aac.
Dasselbe Verfahren funktioniert auch mit FFmpeg 6.1.1; verwenden Sie ein Paket mit aktuellen Patches
für Ihre Distribution. Der Aufruf entfernt geerbte Einstellungen des Ruby-Loaders und von Bundler.
So werden beim Ausführen innerhalb eines übergeordneten Ruby-Projekts nicht dessen Gems ausgewählt.
Wählen Sie ein lokales Video, von dem Sie wissen, dass es einwandfrei ist, mit fester Framegröße und SDR-Farben. Das Skript wählt dessen erste Videospur und, falls vorhanden, dessen erste Audiospur aus. Die Audiospur sollte die ausgewählten Videoabschnitte abdecken. Nicht vertrauenswürdige Uploads, beschädigte Aufnahmen und Dateien, deren Auflösung sich während der Wiedergabe ändert, benötigen eine separate Validierung und einen isolierten Worker; diese CLI bietet diese Isolation nicht. FFmpeg kann sich von einem beschädigten Paket erholen, eine Warnung ausgeben und dennoch Erfolg melden. Ein abgeschlossener Batch bestätigt nicht die Integrität der Quelle; prüfen Sie die Diagnosemeldungen und die erzeugten Ausschnitte.
Schlüsselmomente mit FFmpeg-Filtern extrahieren
Der Szenenwert von FFmpeg schätzt visuelle Unterschiede
zwischen Frames. Für einen manuellen Test in einem neuen Arbeitsverzeichnis schreibt dieser Befehl
die Zeitstempel ausgewählter Frames in timestamps.txt:
ffmpeg -nostdin -n -threads 2 -filter_threads 1 -i input.mp4 -map 0:v:0 \
-vf "select='gt(scene,0.3)',metadata=print:file=timestamps.txt" \
-an -f null -
Belassen Sie Szenenerkennung und Clip-Extraktion auf der eingabebezogenen Zeitachse von FFmpeg.
Setzen Sie die Zeitstempel des Videostreams nicht unabhängig zurück: Audio kann vor dem ersten
Videoframe beginnen. Erkennung und Extraktion wählen beide explizit die erste Videospur aus.
Die Textdatei des Filters ist von den regulären Prüfungen für Ausgabedateien in FFmpeg getrennt:
Verwenden Sie keine vorhandene Datei timestamps.txt erneut.
Die Ruby-Implementierung legt dafür ein privates Job-Verzeichnis an.
Ruby-Skripte zur Automatisierung implementieren
Speichern Sie diese Klasse als highlight_extractor.rb. Sie verwendet feste Dateinamen für die
Ausgabe und übergibt Eingabepfade als separate Prozessargumente. So gelangen Pfade nicht in die
Shell- oder Filtersyntax. Wenn eine Phase fehlschlägt, wird ein Fehler ausgelöst, statt unbemerkt
einen erfolgreichen Batch-Bericht zu erzeugen.
require 'fileutils'
require 'json'
require 'open3'
class HighlightExtractor
def initialize(input_file, output_dir)
@input = File.expand_path(input_file)
raise ArgumentError, 'Input must be a regular file' unless File.file?(@input)
@output = File.expand_path(output_dir)
end
def generate(threshold: 0.3, clip_duration: 5.0, pre_roll: 2.0, min_gap: 10.0)
unless threshold.finite? && (0.0..1.0).cover?(threshold) &&
clip_duration.finite? && clip_duration > 0 &&
pre_roll.finite? && pre_roll >= 0 && min_gap.finite? && min_gap >= clip_duration
raise ArgumentError, 'Invalid scene or clip settings'
end
# Refuse earlier job outputs before taking ownership of this directory.
Dir.mkdir(@output, 0700)
begin
duration = video_duration
selected = detect_scene_changes(threshold).each_with_object([]) do |time, result|
next if time >= duration
result << time if result.empty? || time - result.last >= min_gap
end
# Cap output count so a noisy video cannot create an unlimited clip set.
lengths = []
clips = selected.first(20).each_with_index.map do |time, index|
start = [time - pre_roll, 0.0].max
length = [clip_duration, duration - start].min
lengths << length
name = format('highlight_%03d.mp4', index + 1)
execute('-i', @input, '-ss', start.to_s, '-t', length.to_s,
'-map', '0:v:0', '-map', '0:a:0?', '-c:v', 'libx264', '-preset', 'fast',
'-crf', '23', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-threads', '2', name)
name
end
create_highlight_compilation(clips, lengths) unless clips.empty?
clips.length
rescue StandardError
FileUtils.remove_entry(@output)
raise
end
end
private
def video_duration
output, status = Open3.capture2('ffprobe', '-v', 'error', '-show_entries',
'format=duration:stream=codec_type', '-of', 'json', @input)
raise IOError, 'Cannot inspect input video' unless status.success?
info = JSON.parse(output)
@has_audio = info.fetch('streams').any? { |stream| stream.fetch('codec_type') == 'audio' }
duration = Float(info.fetch('format').fetch('duration'))
raise ArgumentError, 'Invalid video duration' unless duration.finite? && duration > 0
duration
end
def execute(*arguments)
unless system('ffmpeg', '-nostdin', '-n', '-threads', '2', '-filter_threads', '1',
'-filter_complex_threads', '1',
*arguments, chdir: @output)
raise IOError, 'FFmpeg processing failed'
end
end
def detect_scene_changes(threshold)
execute('-i', @input, '-map', '0:v:0', '-vf',
"select='gt(scene,#{threshold})',metadata=print:file=timestamps.txt",
'-an', '-f', 'null', '-')
File.foreach(File.join(@output, 'timestamps.txt')).each_with_object([]) do |line, times|
match = line.match(/pts_time:([0-9.eE+-]+)/)
times << Float(match[1]) if match
end.uniq.sort
end
def create_highlight_compilation(clips, lengths)
list = clips.each_with_index.map do |name, index|
"file '#{name}'\nduration #{lengths[index]}\n"
end.join
File.write(File.join(@output, 'filelist.txt'), list)
arguments = ['-copyts', '-f', 'concat', '-safe', '1', '-i', 'filelist.txt']
if @has_audio
clips.each { |name| arguments.concat(['-i', name]) }
filters = clips.each_index.map do |index|
"[#{index + 1}:a:0]atrim=duration=#{lengths[index]},asetpts=PTS-STARTPTS[a#{index}]"
end
inputs = clips.each_index.map { |index| "[a#{index}]" }.join
filters << "#{inputs}concat=n=#{clips.length}:v=0:a=1[audio]"
arguments.concat(['-filter_complex', filters.join(';'), '-map', '0:v:0', '-map',
'[audio]', '-c:v', 'copy', '-c:a', 'aac'])
else
arguments.concat(['-map', '0:v:0', '-c:v', 'copy'])
end
execute(*arguments, 'highlight_reel.mp4')
end
end
Der Concat-Demuxer benötigt übereinstimmende Streams und Zeitbasen. Diese Clips haben dieselbe Quelle und dieselben Encoder-Einstellungen; die Liste enthält nur erzeugte Basisdateinamen, sodass der Modus für sichere Pfade aktiviert bleibt. Explizite Dauerangaben positionieren jeden Videoclip auf der Zeitachse des Sichtungsvideos. Das Sichtungsvideo kopiert das codierte Video. Wenn Audio vorhanden ist, wird jeder Clip separat decodiert, das Audio auf die angeforderte Cliplänge gekürzt und mit dem Concat-Filter zusammengefügt, bevor es erneut als AAC codiert wird. So summiert sich das Padding der Clip-Encoder nicht zu einer Audioverzögerung. Dieses Sichtungsvideo ist kein lückenloses Audio-Master und kein Werkzeug zum Zusammenfügen beliebiger Dateien. Hören Sie sich die Übergänge vor der Veröffentlichung an.
Der Encoder libx264 verwendet CRF 23 als
Qualitätsziel, ohne Bitratenbegrenzung. Ein kleinerer Wert erhält in der Regel mehr Details und
vergrößert die Datei. Die Voreinstellung fast steuert den Encoding-Aufwand,
nicht die Auswahl der Szenen.
Die ausgabeseitige Option -ss decodiert und verwirft
Aufnahmematerial vor dem Beginn jedes Clips. Dieser einfache Decodierweg benötigt keine nutzbaren
Indizes zum Anspringen von Zeitpunkten, wiederholt aber Arbeit bei späten Highlights. Vergleichen Sie
bei langen, indizierten Aufnahmen die Leistung mit eingabeseitigem, präzisem Anspringen von
Zeitpunkten. Prüfen Sie Clip-Grenzen und den Zeitversatz zwischen Audio und Video, bevor Sie dieses
Verhalten ändern.
Speichern Sie dieses aufrufende Skript neben der Klasse als generate_highlights.rb:
require_relative 'highlight_extractor'
abort 'Usage: ruby generate_highlights.rb <input-video> <new-output-directory>' unless ARGV.length == 2
begin
count = HighlightExtractor.new(*ARGV).generate
puts(count.zero? ? 'No scene changes selected' : "Created #{count} candidate highlights")
rescue StandardError
warn 'Highlight generation failed. Check the input, destination, and FFmpeg installation.'
exit 1
end
Führen Sie es im Terminal aus:
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE \
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Belassen Sie beide Skripte zusammen und ersetzen Sie my_video.mp4 durch Ihren Eingabepfad.
Setzen Sie Pfade mit Leerzeichen in Anführungszeichen. Ein erfolgreicher Durchlauf meldet die Anzahl
der Kandidaten und schreibt highlight_001.mp4, weitere nummerierte Clips und
highlight_reel.mp4 in my_highlights_folder. Öffnen Sie das Sichtungsvideo in Ihrem
Videoplayer. Jeder Clip beginnt standardmäßig 2 Sekunden vor seinem ausgewählten Schnitt, frühestens
jedoch am Beginn der Eingabe, und dauert bis zu 5 Sekunden. Schnitte werden chronologisch mit
mindestens 10 Sekunden Abstand ausgewählt, bis zu einem Maximum von 20 Clips.
Ein vorhandenes Ausgabeverzeichnis wird abgelehnt und bleibt unverändert; wählen Sie für einen erneuten Versuch einen neuen Namen. Ein Verarbeitungsfehler nach dem Anlegen des Job-Verzeichnisses entfernt die unvollständigen Ausgaben dieses Jobs. Beheben Sie das Problem mit der Eingabe oder der Installation und wiederholen Sie denselben Befehl. Bei einem gültigen Video ohne ausgewählte Szenenwechsel wird No scene changes selected ausgegeben. Die Zeitstempeldatei bleibt erhalten, und der Durchlauf endet erfolgreich ohne Clips oder Sichtungsvideo. Lokale FFmpeg-Diagnosemeldungen können Dateinamen enthalten; halten Sie sensible Pfade aus gemeinsam genutzten Logs heraus.
Leistung optimieren
Verarbeiten Sie zuerst eine kurze, repräsentative Aufnahme und prüfen Sie die entstandenen Ausschnitte. Die Obergrenze von zwanzig Clips begrenzt die erzeugten Clips, nicht die Eingabedauer, den vollständigen Szenenanalyse-Durchlauf oder die Ausgabegröße. Halten Sie genügend Speicherplatz für die einzelnen Clips und das Sichtungsvideo frei. Die Anzahl der Decoder- und Encoder-Threads ist hier begrenzt; mehrere gleichzeitig laufende Jobs vervielfachen dennoch deren CPU- und Speichernutzung.
Highlight-Auswahl optimieren
Ein niedrigerer Szenenschwellenwert wählt mehr Änderungen aus, auch Bildrauschen; ein höherer Wert
kann subtile Übergänge übersehen. Die Einstellung min_gap hält die ausgewählten
Zeitstempel mindestens um die angegebene Anzahl von Sekunden auseinander und muss mindestens
clip_duration betragen. Die Auswahl ist chronologisch und keine Rangliste der spannendsten
Momente. Passen Sie Vorlauf und Dauer nach der Sichtung Ihrer Inhalte an.
Praxisbeispiele und Anwendungsfälle
Szenenwechsel-Kandidaten können bei der Sichtung geschnittener Interviews, Folienwechsel und Montageaufnahmen helfen. Gehen Sie nicht davon aus, dass Szenenerkennung Tore, Siege, bestimmte Sprechende oder mündliche Schlussfolgerungen erkennt. Diese Aufgaben benötigen zusätzliche Signale und eine separate Validierung. Das erzeugte Sichtungsvideo ist ein Ausgangspunkt für die redaktionelle Prüfung.
Fazit und mögliche Erweiterungen
Wenn im Sichtungsvideo benötigte Momente fehlen, kann ein niedrigerer Szenenschwellenwert lediglich mehr visuelles Rauschen hinzufügen. Ergänzen Sie eine Audioanalyse oder einen validierten Ereignisdetektor, wenn Ihre Auswahl von Sprache oder Aktivitäten innerhalb einer unveränderten Einstellung abhängt.
Für eine verwaltete Verarbeitung bietet der Video-Encoding-Service von Transloadit FFmpeg-basierte Robots zum Encoding und für Medienworkflows.
