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. |