Ein handgedrehtes Video sieht mit Vorspann gleich professioneller aus. Die Tools mencoder und sox helfen mit der Formatfitzelei, und ein Perlskript automatisiert den Vorgang.
Es ist schon faszinierend, zu wie vielen Themen Youtube Lehrfilme anbietet. Ob ein Hobbykoch sein Leibgericht selbst kochen möchte, der feinmechanisch Interessierte Vorhängeschlösser knacken oder der praktisch veranlagte Autofahrer sein Gefährt reparieren, auf Youtube findet sich oft das passende Video.
Ist so ein Lehrvideo aus dem Rohmaterial erst einmal zusammengeschnipselt, fehlt noch ein Titel. In zwei Sekunden Vorspann kann der Hobbyfilmer mit ein, zwei Zeilen Text darauf hinweisen, was den Zuschauer gleich erwartet. Nun könnte man dies mit proprietären Windows-Programmen wie Adobe Premiere, Mac-Software wie iMovie oder Final Cut, oder gar Linux-Applikationen wie Cinelerra erledigen, doch in der Perl-Kolumne geht es natürlich kurz und schmerzlos von der Kommandozeile aus mit einem kleinen Perlskript, das die beiden Sound- und Videohilfen sox und mencoder einsetzt.
Filme bestehen aus schnell hintereinander abgespielten Einzelbildern, den sogenannten ``Frames''. Normale Videokameras nehmen pro Sekunde etwa 30 davon auf, und ein Programm wie mplayer spielt die Einzelbilder wieder in festen Zeitabständen ab. Ein bewegungsloser Videotitel mit ein bisschen Text lässt sich leicht als eine Reihe identischer JPG-Bilder erzeugen und mit mencoder in eine .avi-Datei umwandeln. Hängt man die beiden Videodateien dann hintereinander, kommt ein Video mit Titel heraus. Wenigstens in der Theorie, in die Toools stellen einige Hürden in den Weg.
Videodateien im .avi-Format dienen als Container für Video- und Audioströme, die ein Videospieler dann zeitgleich abspielt. Sowohl Video- als auch Audiodaten in einem .avi-Container können in verschiedensten Formaten vorliegen. Die Audiospur liegt meist entweder im relativ rohen PCM-Format oder komprimiert als MP3-Daten vor. Videodaten hingegen verbrauchen massenhaft Speicher, wie man sich leicht vorstellen kann, wenn man ausrechnet dass pro Sekunde 30 Bilddateien anfallen. Deswegen spielt das verwendete Kodierungsverfahren, das sogenannte ``Codec'', eine entscheidende Rolle, denn ein gutes Codec kann die Daten extrem komprimieren, ohne die Bildqualität allzusehr in Mitleidenschaft zu ziehen. Codecs gibt es deswegen wie Sand am Meer, und viele sind patentiert, dürfen also nicht einfach nachgebaut werden.
Und obwohl ein .avi-Container verschiedenst kodierte Video- und Audiodaten aufnehmen kann, darf das Kodierungsverfahren nicht einfach mittendrin wechseln. Um also ein Vorspannschnipsel und ein Video hintereinanderzuhängen, muss man entweder dafür sorgen, dass beide die gleichen Codecs verwenden, oder aber mit einem Tool wie mencoder die unterschiedlich kodierten Daten beider in ein gemeinsames Ausgabeformat zu transformieren.
Abbildung 1 zeigt die mit dem Programm in Listing video-meta
ausgelesenen Meta-Daten zweier Videos. Es nutzt das Modul
Video::FrameGrab
vom CPAN, dessen meta
-Methode Kenndaten eines
Videos einholt und in einem Hash ablegt.
Die beiden in Abbildung 1 untersuchten Videos, coolpix.avi
und
camcorder.avi
. Ersteres wurde mit einer kleinen Westentaschenkamera,
einer Nikon Coolpix S52, aufgenommen, das zweite mit einem digitalen
Camcorder der Marke Canon Elura 100. Beide nahmen das Video mit etwa
30 Frames pro Sekunde auf (video_fps
), aber der Canon-Recorder
nutzte die Codec ``ffdv'' (sichtbar im Feld video_codec
) und die Nikon
verwendete ``ffmjpeg''. Auch die Audio-Daten speichern beide Kameras
unterschiedlich. Während der Camcorder zwei Kanäle (also Stereo)
aufnimmt (die Anzahl der Kanäle in audio_nch
ist 2), kann die Nikon nur
Mono. Auch die Audio-Qualität ist unterschiedlich, denn der Camcorder
nimmt Audio mit 32.000 Messpunkten pro Sekunde auf (Feld audio_rate
),
während die Nikon sich mit 8.000 zufrieden gibt.
Abbildung 1 zeigt auch noch, dass die audio_rate
von 8.000 (also die
Anzahl der Messpunkte pro Sekunde) bei der Nikon
einem Wert von 64.000
für die audio_bitrate
(also dem gesamten
Speicherbedarf in bit pro Sekunde) gegenüber steht. An jedem Messpunkt
fallen also genau 8 bit an, also beträgt die sogenannte ``sample size'',
die Breite eines Messpunkts,
genau 1 Byte. Beim Camcorder hingegen fallen pro Audio-Messpunkt 32 bit
(1.024.000 geteilt durch 32.000) an. Pro Kanal sind das 16 bit, also
beträgt die ``sample size'' 2 byte.
Abbildung 1: Meta-Daten zweier Videos, oben: Nikon Coolpix S52, unten: Canon Elura 100. |
Aus diesen Daten ist ersichtlich, dass ein stummes Titelschnipsel nicht ohne Umwandlung vor einem mit einer unbekannten Kamera geschossenen Video stehen kann. Zum Glück bieten die Tools mencoder und sox die notwendigen Funktionen, um die unterschiedlichen Formate so hinzubiegen, dass Titel und Video trotzdem vereint im .avi-Container liegen können.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Data::Dump qw(dump); 04 use Video::FrameGrab; 05 06 my($file) = @ARGV; 07 die "usage: $0 file" unless defined $file; 08 09 my $grabber = Video::FrameGrab->new( 10 video => $file); 11 12 my $meta = $grabber->meta_data(); 13 print dump($meta), "\n";
User, die zum ersten Mal auf mencoder-Kommandos stoßen, wenden sich meist gleich entsetzt ab, denn scheinbar benötigen selbst einfachste Funktionen völlig absurde Kombinationen von Optionen. Näher betrachtet ist die Bedienung aber nicht schwer: Um eine Videodatei in ein anderes Format umzuwandeln, nimmt mencoder die erste Datei als erstes Argument entgegen, erwartet dann die Aktionen zur Umwandlung, gefolgt von der Option -o, der die Ausgabedatei folgt:
mencoder input.avi [Optionen] -o output.avi
Ein Videostrom aus mehreren Eingangsdateien input1.avi, input2.avi, ...
lässt sich ebenfalls einfach erzeugen, in dem man die Dateinamen der
Reihe nach auf der Kommandozeile anstelle von input.avi
hinschreibt.
Optionen für die Umwandlung teilen sich in Audio- und Videokomponenten auf.
Um den Audiostrom der Eingangsdatei unverändert in die Ausgabedatei
zu übernehmen, schreibt man -oac copy
(a für Audio).
Wird der Audio-Track statt
dessen umkodiert, schreibt man statt dessen -oac pcm
für das
PCM-Format (Pulse Code Modulation) oder -oac mp3lame
für das
mit dem Programm lame
erzeugte MP3-Format. Braucht der verwendete Encoder
(hier lame) noch Optionen wie zum Beispiel vbr 3
, hängt man diese
im mencoder-Aufruf unter der Option -lameopts
hintendran:
-oac mp3lame -lameopts vbr=3
.
Entsprechendes gilt für den Video-Teil einer .avi-Datei. Um das Videoformat
1:1 zu übernehmen, wird -ovc copy
(v für Video) verwendet. Um das
Videoformat in das MJPEG-Format umzukodieren, und dem verwendeten
Encoder die Option vcodec=mjpeg
mitzugeben, schreibt man
-ovc lavc -lavcopts vcodec=mjpeg
auf der mencoder-Kommandozeile.
Mit diesem Rüstzeug sollte nun jeder einfach Video-Transformationen
vornehmen können.
Der Vorspann-Generator
in Listing video-title-add
nimmt drei Parameter entgegen:
Die Videodatei, die es mit einem Titel versieht, und zwei Strings, die
es als erste und zweite Zeile im Titelvideo unterbringt. Ruft man es
zum Beispiel mit
video-title-add testvideo.avi \ "Der Geek" "Aufzucht und Hege"
auf, erzeugt es eine neue .avi-Datei testvideo-withtitle.avi
, in der
vor dem eigentlichen Video ein 2 Sekunden langes Titelfilmchen wie in
Abbildung 2 spielt.
Abbildung 2: Der per Skript erzeugte Videovorspann läuft im mplayer vor dem eigentlichen Video. |
Das Skript ruft zunächst die ab Zeile 95 definierte Funktion
jpeg_dir_create
auf, um $n gleiche JPG-Bilder der Breite $w und
der Höhe $h in einem temporären Verzeichnis zu erzeugen. Auf den
Bildern sind die in $upper und $lower übergebenen
Textzeilen auf schwarzem Hintergrund zu sehen. Insgesamt braucht ein
2-Sekunden-Video mit einer Framerate von 30 fps genau 60 Bilder, also
setzt das Hauptprogramm $n auf 60.
Mit dem CPAN-Modul Imager
erzeugt das Skript zunächst es ein neues
Imager-Bildobjekt mit den Maßen $w mal $h. Die Farbe Schwarz definiert
es über ein Objekt der Klasse Imager::Color, das es mit dem RGB-Wert 0-0-0
initialisiert. Der in der Variablen $FONT_FILENAME
gespeicherte Pfad
zeigt zu einer .ttf-Datei mit dem gewünschten Font und ist bei Bedarf
an die lokalen Verhältnisse anzupassen. Das neu erzeugte Font-Objekt bietet
die Methode align()
an, die einen ihr übergebenen String an einer definierten
Stelle ins Bild malt, und die dabei vorgegebene Zentrierung (``center'')
berücksichtigt. Der erste Aufruf von align()
malt die obere Zeile $upper
auf 1/3 der Bildhöhe (von oben gemessen), der zweite Aufruf malt
$lower auf 2/3. Das mittels write()
geschriebene JPG-Bild kommt in
einem neu angelegten temporären Verzeichnis zu liegen. Die for-Schleife
ab Zeile 128 erzeugt zu der eben angelegten Datei c.jpg
noch weitere
59 Hardlinks, sodass mencoder in Zeile 43 glaubt, 60 Dateien
in einem Verzeichnis zu finden, obwohl tatsächlich nur eine einzige dort
steht. Das verwendete Codec ist ``mjpeg'', da die kleine Nikon es
nutzt und die Qualität des zusammengeleimten Videos leidet, falls
eine verlustreiche Kodierung in eine ebenfalls verlustreiche andere
überführt wird.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Sysadm::Install qw(:all); 004 use Imager; 005 use Imager::Fill; 006 use Log::Log4perl qw(:easy); 007 use Video::FrameGrab; 008 use File::Temp qw(tempdir tempfile); 009 010 sub shell; 011 012 my $title_length = 2; # length in seconds 013 my $FONT_FILENAME = "/usr/share/fonts/" . 014 "truetype/ttf-bitstream-vera/VeraSe.ttf"; 015 016 Log::Log4perl->easy_init($ERROR); 017 018 my($video_file, $upper, $lower) = @ARGV; 019 die "usage: $0 ", 020 "video_file upper_text lower_text" 021 unless defined $upper; 022 023 (my $video_out = $video_file) =~ 024 s/(\.[^.]+$)/-withtitle$1/; 025 026 my $video_mum = throwaway_file(".avi"); 027 my $video_title = throwaway_file(".avi"); 028 my $audio_title = throwaway_file(".wav"); 029 my $audio_total = throwaway_file(".wav"); 030 031 my $grabber = Video::FrameGrab->new( 032 video => $video_file); 033 034 my $meta = $grabber->meta_data(); 035 036 my $height = $meta->{video_height}; 037 my $width = $meta->{video_width}; 038 039 my $dir = jpeg_dir_create( 040 $width, $height, $upper, $lower, 041 $meta->{video_fps} * $title_length); 042 043 shell qw(mencoder -nosound), 044 "mf://$dir/*.jpg", 045 qw(-mf fps=30 -o), 046 $video_title, 047 qw(-ovc lavc -lavcopts vcodec=mjpeg); 048 049 my $sample_size = $meta->{audio_bitrate} / 050 $meta->{audio_rate} / 051 $meta->{audio_nch} / 8; 052 053 silent_wav( $title_length, $audio_title, 054 $meta->{audio_rate}, $meta->{audio_nch}, 055 $sample_size ); 056 057 shell qw(mplayer -vc null -vo null -ao 058 pcm), $video_file; 059 060 shell "sox", $audio_title, 061 "audiodump.wav", "-o", $audio_total; 062 063 shell "mencoder", "-nosound", $video_title, 064 $video_file, qw(-ovc lavc -lavcopts 065 vcodec=mjpeg -o), $video_mum; 066 067 # add sound 068 shell "mencoder", $video_mum, qw(-oac copy 069 -audiofile), $audio_total, 070 qw(-ovc copy -o), $video_out; 071 072 ########################################### 073 sub throwaway_file { 074 ########################################### 075 my($suffix) = @_; 076 077 my($fh, $file) = tempfile( 078 UNLINK => 1, 079 SUFFIX => $suffix, 080 ); 081 return $file; 082 } 083 084 ########################################### 085 sub shell { 086 ########################################### 087 my($stdout, $stderr, $rc) = tap @_; 088 089 if($rc) { 090 die "Command @_ failed: $stderr"; 091 } 092 } 093 094 ########################################### 095 sub jpeg_dir_create { 096 ########################################### 097 my($w, $h, $upper, $lower, $n) = @_; 098 099 my $img = Imager->new(xsize => $width, 100 ysize => $height); 101 102 my $black = Imager::Color->new( 0,0,0 ); 103 $img->box(color=> $black, filled => 1); 104 105 106 my $font = Imager::Font->new( file => 107 $FONT_FILENAME) or die Imager->errstr; 108 109 $font->align(string => $upper, 110 size => 38, color => "white", 111 x => $width/2, y => $height/3, 112 halign => "center", valign => "center", 113 image => $img ); 114 115 $font->align(string => $lower, 116 size => 38, color => "white", 117 x => $width/2, y => $height*2/3, 118 halign => "center", valign => "center", 119 image => $img ); 120 121 my($dir) = tempdir( CLEANUP => 1 ); 122 123 my $img_file = "$dir/c.jpg"; 124 125 $img->write(file => $img_file) or 126 die "Cannot write ($!)"; 127 128 for (1..$n-1) { 129 cd $dir; 130 (my $link = $img_file) =~ s/\./$_./; 131 link $img_file, $link or die $!; 132 cdback; 133 } 134 135 return $dir; 136 } 137 138 ########################################### 139 sub silent_wav { 140 ########################################### 141 my($secs, $outfile, $rate, $channels, 142 $sample_size) = @_; 143 144 my($fh, $tempfile) = 145 tempfile( UNLINK => 1, 146 SUFFIX => ".dat" ); 147 148 print $fh "; SampleRate $rate\n"; 149 my $samples = $secs * $rate; 150 151 for (my $i = 0; ($i < $samples); $i++) { 152 print $fh $i / $rate, "\t0\n"; 153 } 154 close $fh; 155 156 shell "sox", $tempfile, "-r", $rate, 157 "-u", "-$sample_size", "-c", 158 $channels, $outfile; 159 }
Das in Zeile 43 mit mencoder geschriebene Titelvideo trägt nun
keinerlei Tonspur, denn JPG-Bildern ist kein Audio zugeordnet und
mencoder wurde mit -noaudio
ruhig gestellt. Ein Video ohne
Ton kann man aber nicht mit einem mit Tonspur zusammenschweißen, also
muss das Skript nun eine Sounddatei mit 2 Sekunden Stille produzieren.
Abbildung 3: Die Rohdaten einer 2 Sekunden langen, stillen Audiodatei. |
Die ab Zeile 139 definierte Funktion silent.wav nimmt dazu die Anzahl der gewünschten Sekunden, den Namen der Ergebnisdatei, die Anzahl der Messpunkte pro Sekunden ($rate), die Anzahl der Kanäle ($channels) und die Byte-Breite eines Messpunktes ($sample_size) entgegen. In einer neu angelegten temporären Datei mit dem Suffix .dat legt es die Rohdaten als Nullbytes ab. Die Utility sox greift sie sich in Zeile 156 und macht daraus die geforderte .wav-Datei.
Zurück im Hauptprogramm wäre es nun eigentlich folgerichtig, die Sounddatei mit dem Titelvideo zu verbandeln und dann mit mencoder beide .avi-Dateien aneinander zu hängen. Doch leider schafft mencoder dies nicht, ohne die Audio-Tracks auf übelste Art und Weise zu verschieben, was zu unzumutbaren Synchronisationsproblemen von Audio- und Videospur im Ergebnisvideo führt. Was hingegen tadellos funktioniert, ist die Audiospur des Originalvideos zu extrahieren, sie mit dem vorher erzeugten Stillaudio zusammen zu schweißen, und die entstehende Gesamtaudiospur mit den zwei aneinandergereihten tonlosen Videos zu verschmelzen.
Der mplayer-Aufruf in Zeile 57 extrahiert die Audiospur des Originalvideos
in die Datei audiodump.wmv
. Zeile 60 legt die stille Tonspur davor und
erzeugt so die Gesamttonspur in der Datei $audio_total.
Zeile 63 wirft den mencoder an und hängt
$video_title und $video_file mit der Option -nosound hintereinander
und konvertiert das Ergebnis eine .avi-Datei, deren Video-Stream
als MJPEG-Codec vorliegt.
Eigentlich sollte
man meinen, dass mencoder die aus den JPEG-Fotos erzeugte Video-Datei im
MJPEG-Format an ein mit einer Kamera im MJPEG-Format erzeugtes Video
anhängen könnte, ohne am Codec herumzufummeln, aber mencoder brach mit
einer Fehlermeldung ab, die darauf hindeutete, dass es mit der
verwendeten Kodierung nicht zurechtkam. Falls mencoder die Camcorderdatei
allerdings selbst nach MJPEG umwandelt, funktioniert das spätere
Anhängen tadellos. Das ist eigentlich schade, denn das Umkodieren
dauert fast so lange, wie das Video spielt, während die Option
-ovc copy
um ein vielfaches schneller durch das Format rast. Aber
was hilft's!
Nun fehlt noch, die vorher angelegte Gesamttonspur $audio_total
in das eben
erzeugte, noch tonlose Gesamtvideo einzubinden. Der mencoder-Aufruf
in Zeile 68 mit der Option -audiofile
erledigt genau das,
lässt die Videokodierung mit -ovc copy
in Frieden und
schreibt das Ergebnis-.avi in die Datei, deren Name in $video_out
liegt, also testvideo-withtitle.avi
.
Das Skript nutzt einige Utility-Funktionen, die es zum Teil selbst
definiert und zum Teil aus dem CPAN-Modul Sysadm::Install zieht.
Die ab Zeile 73 definierte Funktion throwaway_file()
erzeugt
zum Beispiel eine temporäre Datei mit der in $suffix geforderten
Endung. Dies ist wichtig, denn manche Utilities schließen von der
Dateiendung auf das dort verwendete Format. Die temporären Dateien
verwaltet das CPAN-Modul File::Temp und löscht sie erst bei
Skriptende automatisch.
Die ab Zeile 85 definierte Funktion shell
führt ein als Liste
übergebenes Shell-Kommando mit Parametern aus, prüft, ob alles
klar ging, und bricht das Programm ab, falls etwas schief lief.
Die Deklaration der Funktion in Zeile 10 von video-title-add
dient lediglich
dazu, später den klammerlosen Aufruf der Funktion shell
zu erlauben.
shell
nutzt die Funktion tap
aus dem CPAN-Modul Sysadm::Install,
die ein externes Programm aufruft, die Standardaus- und Errorausgabe
abfängt und mit dem Rückgabecode zurückliefert.
Die beiden Tools mencoder und sox sind auf Linux-Systemen oft schon installiert, und andernfalls lassen sie sich zum Beispiel auf Debian mit
sudo apt-get install sox mencoder
installieren. Die CPAN-Module Sysadm::Install, Log::Log4perl,
Imager, und Imager::Fill sind ebenfalls als Debian-Pakete verfügbar.
Falls dies für die verwendete Distribution nicht zutrifft, hilft
eine CPAN-Shell bei der Installation. Das Modul Video::FrameGrab
muss auf jeden Fall so installiert werden. Der in Zeile 13 defininierte
Pfad zur True-Type-Fontdatei für den verwendeten Font VeraSe.ttf
ist unter Umständen an die lokalen Gegebenheiten der Distro anzupassen.
Neben einem Titel trägt auch ein Abspann gewinnbringend zum Wert eines Videos bei. Hierzu erweitert man das Skript einfach, und läßt es einen zweiten Stummfilm mit dem Abspann erzeugen, präsentiert dazu eine stille Sounddatei $audio_trailer (oder benutzt das bereits erzeugte $audio_title, falls Abspann und Titel genau gleich lang sind) und hängt diese in den sox-Aufruf von Zeile 60 mit ein:
shell "sox", $audio_title, "audiodump.wav", $audio_trailer, "-o", $audio_total;
Das aus JPEG-Bildern genau wie $video_title erzeugte stille Abspann-Video $video_trailer wird dann hinter den Parameter $video_file in den mencoder-Aufruf von Zeile 63 eingehängt. Der Kamermann freut sich sicher über die Erwähnung seines Namens in den sogenannten ``Credits'', und Links ins Web verweisen Interessierte dort auf weiterführende Informationen.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/01/Perl
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |