Video-Highlights mit Ruby und FFmpeg automatisieren
Nützliche Momente in langem Filmmaterial zu finden, kostet Zeit. Ruby kann die Erkennung von Szenenwechseln automatisieren, mögliche Clips extrahieren und sie mit FFmpeg zu einem Sichtungsreel zusammenfügen. Das ist eine Heuristik für visuelle Veränderungen und kein System, das versteht, welche Momente wichtig sind.
Einführung in automatisierte Video-Highlights
Der Workflow erkennt große Unterschiede zwischen benachbarten Frames, wählt ausreichend weit auseinanderliegende Zeitstempel aus und codiert kurze Clips um diese herum. Ein Schnitt, ein Blitz oder ein Folienwechsel kann ein nützlicher Kandidat sein, doch ein Tor oder eine gesprochene Erkenntnis hat möglicherweise keinen Szenenschnitt. Prüfen Sie das erzeugte Reel, bevor Sie es veröffentlichen.
Ruby und FFmpeg einrichten
Installieren Sie Ruby, FFmpeg und FFprobe. Unter macOS:
brew install ruby ffmpeg
Unter Ubuntu/Debian:
sudo apt-get update
sudo apt-get install ruby ffmpeg
Das Beispiel nutzt Rubys Standardbibliothek statt eines FFmpeg-Wrappers von Drittanbietern. Prüfen Sie die Werkzeuge:
ruby -v
ffmpeg -version
ffprobe -version
Versionskompatibilität
Verwenden Sie eine gepflegte Ruby-Version unter Linux oder macOS und einen FFmpeg-Build mit den
Filtern select und metadata sowie dem Encoder libx264. Diese CLI verarbeitet jeweils
ein vertrauenswürdiges lokales Video. Für nicht vertrauenswürdige Uploads verwenden Sie einen
isolierten Worker mit Limits für Dateisystem, Netzwerk, Festplatte, CPU und Echtzeit-Laufzeit. Ein
Decoder-Prozess allein ist keine Sicherheitsgrenze.
Schlüsselmomente mit FFmpeg-Filtern extrahieren
Der Szenen-Score schätzt visuelle Unterschiede zwischen Frames. Für eine manuelle Prüfung in einem
leeren Arbeitsverzeichnis schreibt dieser Befehl die Zeitstempel ausgewählter Frames in
timestamps.txt:
ffmpeg -nostdin -n -i input.mp4 -map 0:v:0 \
-vf "select='gt(scene,0.3)',metadata=print:file=timestamps.txt" \
-an -f null -
Halten Sie Szenenerkennung und Clip-Extraktion auf der eingabebezogenen Zeitachse von FFmpeg.
Setzen Sie die Zeitstempel des Videostreams nicht unabhängig davon zurück: Audio kann vor dem
ersten Videoframe beginnen. Erkennung und Extraktion wählen beide ausdrücklich dieselbe erste
Videospur aus. Die Textdatei des Filters unterliegt nicht FFmpegs üblichen Prüfungen für
Ausgabedateien: Verwenden Sie eine bereits vorhandene Datei timestamps.txt nicht erneut. Die
Ruby-Implementierung legt dafür ein eigenes Job-Verzeichnis an.
Ruby-Skripte für die Automatisierung implementieren
Speichern Sie diese Klasse als highlight_extractor.rb. Sie verwendet fest vorgegebene Dateinamen für
die erzeugten Ausgaben und übergibt Eingabepfade als separate Prozessargumente, sodass Pfade nicht
in die Shell- oder Filtersyntax gelangen. Eine fehlgeschlagene Stufe löst einen Fehler aus, anstatt
stillschweigend 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.
clips = selected.first(20).each_with_index.map do |time, index|
start = [time - pre_roll, 0.0].max
length = [clip_duration, duration - start].min
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',
'-pix_fmt', 'yuv420p', '-c:a', 'aac', '-threads', '2', name)
name
end
create_highlight_compilation(clips) 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', '-of', 'json', @input)
raise IOError, 'Cannot inspect input video' unless status.success?
duration = Float(JSON.parse(output).fetch('format').fetch('duration'))
raise ArgumentError, 'Invalid video duration' unless duration.finite? && duration > 0
duration
end
def execute(*arguments)
unless system('ffmpeg', '-nostdin', '-n', *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)
File.write(File.join(@output, 'filelist.txt'), clips.map { |name| "file '#{name}'\n" }.join)
execute('-f', 'concat', '-safe', '1', '-i', 'filelist.txt', '-c', 'copy', 'highlight_reel.mp4')
end
end
Die concat-Liste enthält nur generierte Basisnamen, sodass FFmpegs Safe-Path-Modus aktiviert bleibt. Alle Clips stammen aus derselben Quelle und denselben Encoder-Einstellungen. Audio ist optional, dieses Beispiel ist jedoch kein universeller Concatenator für nicht zusammengehörige Dateien mit unterschiedlichen Stream-Layouts.
Die ausgabeseitige Option -ss decodiert und verwirft das Filmmaterial vor der Startzeit
jedes Clips. Dieser unkomplizierte Decodierweg ist nicht auf nutzbare Suchindizes angewiesen,
wiederholt jedoch Arbeit für spät liegende Highlights. Bei langen, indizierten Aufnahmen sollten
Sie das eingabeseitige exakte Suchen messen und Clip-Grenzen sowie Audio-/Video-Versätze prüfen,
bevor Sie dieses Verhalten ändern.
Speichern Sie dieses Aufrufskript 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 in Ihrem Terminal aus:
ruby generate_highlights.rb my_video.mp4 my_highlights_folder
Ein bereits vorhandenes Ausgabeverzeichnis wird abgelehnt und bleibt unverändert. Schlägt etwas nach dem Anlegen des Job-Verzeichnisses fehl, werden die unvollständigen Ausgaben dieses Jobs entfernt. Ein gültiges Video ohne ausgewählte Szenenwechsel wird erfolgreich verarbeitet, mit einer klaren Meldung und ohne Kompilationsdatei. Lokale FFmpeg-Diagnose-Logs können Dateinamen enthalten; leiten Sie sie nicht an Web-Clients weiter und verwenden Sie keine sensiblen Dateinamen in geteilten Logs.
Leistungsoptimierung
- Verarbeiten Sie einen kurzen, repräsentativen Clip vor einer langen Aufnahme.
- Begrenzen Sie die Anzahl gleichzeitiger Worker und überwachen Sie den Speicherplatzverbrauch auf der Festplatte. Die Obergrenze von zwanzig Clips begrenzt die erzeugten Clips, nicht die Eingabedauer oder den Aufwand der Szenenanalyse.
- Wenden Sie ein Echtzeit-Laufzeitlimit auf den gesamten Worker an und beenden Sie bei einem Abbruch dessen Kindprozesse. Das Beispiel implementiert keinen Prozess-Supervisor und sichert keine Durchsetzung von Timeouts pro Job zu.
- Wählen Sie Hardware-Encoder erst, nachdem Sie Qualität und Stream-Kompatibilität validiert haben; ein schnellerer Encoder kann andere Flags und Ausgabeeinstellungen erfordern.
Auswahl der Highlights optimieren
Ein niedrigerer Szenen-Schwellenwert wählt mehr Veränderungen aus, einschließlich Rauschen; ein
höherer Schwellenwert kann subtile Übergänge übersehen. Die Einstellung min_gap hält ausgewählte
Zeitstempel um mindestens so viele Sekunden auseinander und muss mindestens clip_duration betragen. Die
Auswahl erfolgt chronologisch und ist keine Rangfolge der spannendsten Momente. Passen Sie Vorlauf
und Dauer an, nachdem Sie Ihre Inhalte geprüft haben.
Praktische Beispiele und Anwendungsfälle
Kandidaten für Szenenwechsel können beim Sichten geschnittener Interviews, von Folienwechseln und von Montagematerial helfen. Gehen Sie nicht davon aus, dass die Szenenerkennung Tore, Siege, bestimmte Sprecher oder gesprochene Schlussfolgerungen erkennt. Diese Aufgaben erfordern zusätzliche Signale und eine separate Validierung. Das erzeugte Reel ist ein Ausgangspunkt für die redaktionelle Prüfung.
Fazit und mögliche Erweiterungen
Ruby kann die dokumentierten Schnittstellen von FFmpeg ohne Wrapper-Abhängigkeit orchestrieren, Job-Ausgaben getrennt halten und fehlgeschlagene Stufen sichtbar machen. Ergänzen Sie eine Audioanalyse oder einen validierten Ereignisdetektor nur dann, wenn Szenenwechsel für Ihren Anwendungsfall nicht ausreichen.
Für verwaltete Verarbeitung bietet der Video-Encoding-Service von Transloadit FFmpeg-basierte Robots für Encoding und Medien-Workflows.
