Videos mit Ruby und FFmpeg in HLS und MPEG-DASH konvertieren
Paketieren Sie ein lokales Video mit Ruby und FFmpeg für die Wiedergabe auf Abruf. Dieses Beispiel setzt drei HLS-Qualitätsstufen, eine Master-Playlist mit gemessener Segmentbandbreite und ein DASH-Manifest mit zwei Videorepräsentationen um. Sie stellen die fertigen Dateien über Loopback-HTTP bereit und spielen beide Formate im Browser ab.
Einführung in HLS und MPEG-DASH
HLS verwendet Playlists im Format .m3u8, MPEG-DASH ein Manifest im Format
.mpd. Jeder Index verweist auf Mediensegmente.
Mehrere kompatible Qualitätsstufen ermöglichen dem Player die Auswahl einer Qualität. Die
Paketierung allein belegt jedoch weder eine reibungslose Anpassung noch weniger Pufferpausen in
einem realen Netzwerk. Die hier verwendete Bitratenleiter ist ein Beispiel, keine Empfehlung für
jedes Video.
Ruby-Umgebung für die Videokonvertierung einrichten
Verwenden Sie Bash unter Linux mit Ruby, RubyGems, FFmpeg, ffprobe und Python im
PATH. Die getestete Umgebung verwendet Ruby 3.4.10, FFmpeg/ffprobe 9.0.1
und Python 3.14.7. Wählen Sie eine
weiterhin gepflegte Ruby-Version; Ruby 3.2 hat sein Supportende erreicht.
Ihr FFmpeg-Build benötigt die Encoder libx264 und
aac sowie die HLS- und DASH-Muxer:
ruby -v && gem -v && python3 --version &&
ffmpeg -version && ffprobe -version && ffmpeg -encoders && ffmpeg -muxers
Fügen Sie den folgenden Block in Bash ein, während Sie sich in einem beschreibbaren Verzeichnis
befinden. Er erstellt ein neues Projekt und installiert darin Gems mit den expliziten
Optionen für das Installationsverzeichnis von RubyGems.
Ein bereits vorhandenes Verzeichnis video_conversion stoppt die Einrichtung. Schlägt die
Installation fehl, kehren Sie in das ursprüngliche Verzeichnis zurück. Prüfen Sie das neue
Verzeichnis vor einem erneuten Versuch, statt vorhandene Arbeit zu löschen.
(
unset RUBYOPT RUBYLIB BUNDLE_GEMFILE
mkdir video_conversion &&
cd video_conversion &&
export GEM_HOME="$PWD/.gems" GEM_PATH="$PWD/.gems" GEM_SPEC_CACHE="$PWD/.gem-specs" &&
gem install streamio-ffmpeg -v 3.0.2 --norc --no-document --no-user-install --install-dir "$GEM_HOME" &&
gem install logger -v 1.7.0 --norc --no-document --no-user-install --install-dir "$GEM_HOME"
) && cd video_conversion
Diese eigenständige Einrichtung entfernt geerbte Ruby-Loader-Einstellungen innerhalb der Subshell,
damit ein übergeordnetes Bundler-Projekt nicht dessen Gems auswählt. Der unten stehende Befehl zum
Ausführen nutzt dieselbe Isolation.
Kopieren Sie eine lokale Datei, von der Sie wissen, dass sie funktioniert, als
input.mp4 in das Projekt: Verwenden Sie SDR-H.264-Video mit quadratischen Pixeln,
einem 16:9-Bild mit mindestens 1280×720 und einer Audiospur. Andere Seitenverhältnisse würden durch
diese feste Bitratenleiter verzerrt. Wählen Sie einen Clip mit mehr als 20 Sekunden Länge, um mehrere
Segmentgrenzen zu testen. Videos ohne Ton benötigen eine andere Zuordnungs- und Playlist-Logik.
Videos in Ruby ins HLS-Format konvertieren
Das Streamio FFMPEG-Gem stellt die Eingabeanalyse und Encoder-Optionen bereit. Seine neueste veröffentlichte Version ist 3.0.2, erschienen 2016. Die Kompatibilitätshinweise des Originalprojekts beziehen sich auf deutlich ältere FFmpeg-Versionen. Dieses Beispiel testet den begrenzten Einsatz des Gems mit den oben genannten Versionen und ruft FFmpeg direkt mit einem Argument-Array und geprüftem Exit-Status auf. Jede Konvertierung erstellt ein neues Ausgabeverzeichnis. Ein vorhandenes Verzeichnis wird abgelehnt, um frühere Playlists und Segmente zu schützen.
Erstellen Sie eine neue Datei namens video_converter.rb:
require 'streamio-ffmpeg'
require 'fileutils'
class VideoConverter
def initialize(input_file)
begin
@movie = FFMPEG::Movie.new(input_file)
raise "Invalid file" unless @movie.valid?
@input_file = input_file
rescue FFMPEG::Error => e
raise "FFmpeg error: #{e.message}"
rescue StandardError => e
raise "Error initializing converter: #{e.message}"
end
end
def to_hls(output_dir)
FileUtils.mkdir_p(File.dirname(output_dir))
Dir.mkdir(output_dir)
variants = [
{ resolution: '1280x720', video_bitrate: '2500k', audio_bitrate: '128k' },
{ resolution: '854x480', video_bitrate: '1500k', audio_bitrate: '96k' },
{ resolution: '640x360', video_bitrate: '800k', audio_bitrate: '64k' }
]
begin
variants.each do |variant|
options = {
video_codec: 'libx264',
audio_codec: 'aac',
frame_rate: 30,
resolution: variant[:resolution],
video_bitrate: variant[:video_bitrate],
audio_bitrate: variant[:audio_bitrate],
custom: [
'-map', '0:v:0', '-map', '0:a:0',
'-pix_fmt', 'yuv420p', '-threads', '2',
'-g', '300', '-keyint_min', '300', '-sc_threshold', '0',
'-force_key_frames', 'expr:gte(t,n_forced*10)',
'-hls_time', '10',
'-hls_playlist_type', 'vod',
'-hls_segment_filename', "#{output_dir}/#{variant[:resolution]}_%03d.ts"
]
}
transcode("#{output_dir}/#{variant[:resolution]}.m3u8", options)
end
generate_master_playlist(output_dir, variants)
rescue FFMPEG::Error => e
raise "Transcoding error: #{e.message}"
rescue StandardError => e
raise "General error: #{e.message}"
end
end
def to_dash(output_dir)
FileUtils.mkdir_p(File.dirname(output_dir))
Dir.mkdir(output_dir)
begin
options = {
video_codec: 'libx264',
audio_codec: 'aac',
frame_rate: 30,
custom: [
'-map', '0:v:0', '-map', '0:v:0', '-map', '0:a:0',
'-pix_fmt', 'yuv420p', '-threads', '2',
'-s:v:0', '1280x720', '-b:v:0', '2500k',
'-s:v:1', '640x360', '-b:v:1', '800k',
'-g', '300', '-keyint_min', '300', '-sc_threshold', '0',
'-force_key_frames', 'expr:gte(t,n_forced*10)',
'-use_template', '1',
'-use_timeline', '1',
'-seg_duration', '10',
'-adaptation_sets', 'id=0,streams=v id=1,streams=a',
'-f', 'dash'
]
}
transcode("#{output_dir}/manifest.mpd", options)
rescue FFMPEG::Error => e
raise "DASH conversion error: #{e.message}"
rescue StandardError => e
raise "General error: #{e.message}"
end
end
private
def transcode(output_file, options)
arguments = FFMPEG::EncodingOptions.new(options).to_a
system(FFMPEG.ffmpeg_binary, '-nostdin', '-n', '-i', @input_file,
*arguments, output_file, exception: true)
end
def generate_master_playlist(output_dir, variants)
master_playlist = "#EXTM3U\n#EXT-X-VERSION:3\n"
variants.each do |variant|
# Measure the multiplexed segments, including audio and container overhead.
duration = nil
target_duration = nil
segments = []
File.foreach("#{output_dir}/#{variant[:resolution]}.m3u8") do |line|
if line.start_with?('#EXT-X-TARGETDURATION:')
target_duration = Integer(line.split(':', 2).last)
elsif line.start_with?('#EXTINF:')
duration = Float(line.delete_prefix('#EXTINF:').split(',').first)
elsif !line.start_with?('#') && !line.strip.empty?
raise 'Missing segment duration' unless duration && duration.positive?
segments << { bytes: File.size(File.join(output_dir, line.strip)), duration: duration }
duration = nil
end
end
raise 'Missing HLS target duration' unless target_duration && target_duration.positive?
rates = []
segments.each_index do |first|
bytes = 0
seconds = 0.0
(first...segments.length).each do |last|
bytes += segments[last][:bytes]
seconds += segments[last][:duration]
break if seconds > 1.5 * target_duration
rates << bytes * 8 / seconds if seconds >= 0.5 * target_duration
end
end
raise 'No measurable HLS segment window' if rates.empty?
bandwidth = rates.max.ceil
master_playlist += <<~PLAYLIST
#EXT-X-STREAM-INF:BANDWIDTH=#{bandwidth},RESOLUTION=#{variant[:resolution]}
#{variant[:resolution]}.m3u8
PLAYLIST
end
File.write("#{output_dir}/master.m3u8", master_playlist)
end
end
Sobald alle drei Medien-Playlists fertig sind, schreibt Ruby master.m3u8. Für jede
Qualitätsstufe misst es zusammenhängende Segmentfenster, deren Dauer zwischen dem 0,5-Fachen und dem
1,5-Fachen der Zieldauer der Playlist liegt. Die höchste Rate wird für
BANDWIDTH aufgerundet, entsprechend der
HLS-Definition der maximalen Segmentbitrate. Dabei werden Audio und
MPEG-TS-Overhead berücksichtigt. Ein kurzes letztes Segment wird zusammen mit seinem Nachbarsegment
gemessen, wenn es allein zu kurz für die Messung ist. Die Bitrate des Video-Encoders zu verwenden,
würde einen Teil der ausgelieferten Daten auslassen.
Videos in Ruby in MPEG-DASH konvertieren
Die obige Methode to_dash ordnet das Eingabevideo zweimal zu und erstellt so
Repräsentationen in 720p und 360p sowie eine gemeinsame Audiorepräsentation. Beide Video-Encodings
verwenden dieselbe Bildrate und erzwingen alle zehn Sekunden Keyframes, damit die Segmentgrenzen
übereinstimmen. HLS nutzt für seine drei Qualitätsstufen denselben Keyframe-Zeitplan. Das letzte
Segment kann kürzer als zehn Sekunden sein. Die FFmpeg-Dokumentation der Muxer für
HLS und
DASH erläutert die Paketierungsoptionen.
Konverter verwenden
Speichern Sie diese zweite Datei als convert.rb neben
video_converter.rb und input.mp4:
require_relative 'video_converter'
begin
converter = VideoConverter.new('input.mp4')
puts 'Converting to HLS…'
converter.to_hls('output/hls')
puts 'Converting to MPEG-DASH…'
converter.to_dash('output/dash')
puts 'Conversion completed successfully'
rescue StandardError => e
warn "Error: #{e.message}"
exit 1
end
Führen Sie den Befehl im Verzeichnis video_conversion aus:
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE \
GEM_HOME="$PWD/.gems" GEM_PATH="$PWD/.gems" ruby convert.rb
Warten Sie auf Conversion completed successfully. Das Ergebnis umfasst
output/hls/master.m3u8, die zugehörigen drei Medien-Playlists und Segmente im Format
.ts sowie output/dash/manifest.mpd, Initialisierungsdateien und
Segmente im Format .m4s. Bewahren Sie jeden Index zusammen mit den Dateien
auf, auf die er verweist. Bei erneuter Ausführung lehnt das Skript das vorhandene Verzeichnis
output/hls ab.
Streams testen
Speichern Sie diese Seite als output/player.html. Jeder Player hat eine Quelle, sodass
ein HLS-Test nicht unbemerkt einen DASH-Test ersetzen kann.
Video.js HTTP Streaming ist im Standard-Build von Video.js enthalten
und unterstützt beide Formate über Media Source Extensions. Bibliothek und Stylesheet werden von
einem CDN geladen, daher benötigt diese Seite Internetzugang.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Local HLS and DASH playback</title>
<link href="https://cdn.jsdelivr.net/npm/video.js@8.24.0/dist/video-js.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/video.js@8.24.0/dist/video.min.js"></script>
<style>html { color-scheme: light dark; }</style>
</head>
<body>
<section aria-label="HLS">
<h1>HLS</h1>
<video id="hls" class="video-js" controls preload="metadata" width="640" height="360">
<source src="hls/master.m3u8" type="application/x-mpegURL" />
</video>
</section>
<section aria-label="MPEG-DASH">
<h2>MPEG-DASH</h2>
<video id="dash" class="video-js" controls preload="metadata" width="640" height="360">
<source src="dash/manifest.mpd" type="application/dash+xml" />
</video>
</section>
<script>
videojs('hls', { fluid: true })
videojs('dash', { fluid: true })
</script>
</body>
</html>
Starten Sie im Verzeichnis video_conversion den lokalen HTTP-Server von Python im
Vordergrund:
python3 -m http.server 8080 --bind 127.0.0.1 --directory output
Öffnen Sie http://127.0.0.1:8080/player.html in einem Browser, der H.264/AAC und Media Source Extensions
unterstützt. Spielen Sie jedes Video bis zum Ende ab und springen Sie anschließend über eine
Segmentgrenze hinweg. Diese Seite wurde mit Video.js 8.24.0 und Chromium 145 unter Linux geprüft.
Stoppen Sie den Server mit Strg+C. Ist Port 8080 belegt, wählen Sie im Befehl einen anderen Port und
verwenden Sie ihn auch in der URL. Der
HTTP-Server von Python stellt hier nur das Verzeichnis
output bereit. Beschränken Sie diesen lokalen Test auf öffentliche Testmedien.
Fehler und fertige Medien prüfen
Eine Eingabe, die sich nicht öffnen oder erkennen lässt, eine fehlende Audiospur oder ein
fehlgeschlagener FFmpeg-Prozess führen dazu, dass convert.rb mit einem Status
ungleich null endet. Die Diagnosemeldungen von FFmpeg nennen die betroffene Eingabe oder Ausgabe.
Ein fehlgeschlagener Durchlauf kann unvollständige Dateien im neuen Verzeichnis hinterlassen.
Gelingt HLS und schlägt danach DASH fehl, bleiben die fertigen HLS-Dateien erhalten. Veröffentlichen
Sie erst, wenn das gesamte Skript erfolgreich abgeschlossen ist. Verwenden Sie für einen erneuten
Versuch neue Ausgabeverzeichnisnamen in convert.rb und passen Sie die Quellpfade
des Players entsprechend an.
FFmpeg kann beschädigte Eingaben verarbeiten und trotzdem null zurückgeben. Eine erfolgreiche Konvertierung ist keine Integritätsprüfung: Vergleichen Sie Bild, Ton und Dauer des Ergebnisses mit Ihrer Quelle. Prüfen Sie alle drei HLS-Qualitätsstufen und beide DASH-Videorepräsentationen, nicht nur die vom Player gewählte Qualität. Stellen Sie sicher, dass jeder Verweis in einer Playlist oder einem Manifest aufgelöst wird und die Wiedergabe das erwartete Ende erreicht. Ein Manifest-Dateiname oder ein erfolgreich wiedergegebenes erstes Bild belegt keine vollständige Wiedergabe.
Über das lokale Beispiel hinausgehen
Die HLS-Methode codiert die Eingabe dreimal nacheinander. DASH erstellt beide Videorepräsentationen in einem FFmpeg-Prozess. Beginnen Sie mit einem Konvertierungsauftrag und messen Sie CPU-, Arbeitsspeicher- und Festplattennutzung, bevor Sie weitere Worker hinzufügen. Diese Subprozessaufrufe mit Statusprüfung bieten weder ein Timeout noch eine Isolationsgrenze für nicht vertrauenswürdige Uploads. Ein Auslieferungsdienst für den Produktivbetrieb benötigt außerdem eine eigene Veröffentlichungs- und HTTP-Einrichtung. Diese Anleitung überprüft die Paketierung und die lokale Wiedergabe.
Fazit
Bewahren Sie die fertigen Manifeste und ihre Segmente zusammen auf, wenn Sie das Ergebnis in Ihre Veröffentlichungsumgebung übertragen. Für verwaltetes Encoding entdecken Sie den Video-Encoding-Dienst von Transloadit.
