Ein handgedrehtes Video sieht mit einem Vorspann gleich professioneller aus. Die Tools Mencoder und Sox helfen bei der Formatfitzelei und ein Perl-Skript automatisiert den Vorgang .
Es ist schon faszinierend, zu wie vielen Themen Youtube Lehrfilme anbietet. Ob ein Hobbykoch sein Leibgericht kochen oder der praktisch veranlagte Autofahrer sein Gefährt reparieren will, auf Youtube findet sich oft Passendes. Ist so ein Lehrvideo 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.
Das erledigen proprietäre Windows-Programme wie Adobe Premiere, Mac-Software wie I-Movie oder gar Linux-Applikationen wie Cinelerra, doch in der Perl-Kolumne geht es natürlich kurz und schmerzlos von der Kommandozeile aus mit einem kleinen Perl-Skript.
Laufende Bilder
Filme bestehen aus schnell hintereinander abgespielten Einzelbildern, den so genannten Frames. Normale Videokameras nehmen pro Sekunde etwa 30 davon auf, 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 Jpeg-Bilder erzeugen und mit Mencoder [1] in eine AVI-Datei umwandeln. Wer die beiden Videodateien dann hintereinanderhängt, erhält ein Video mit Titel – wenigstens in der Theorie. In der Praxis stehen doch noch ein paar Hürden im Weg.
Videodateien im AVI-Format dienen als Container für Video- und Audioströme, die ein Player gleichzeitig abspielt. Sowohl Video- als auch Audiodaten in einem AVI-Container können in verschiedenen Formaten gespeichert sein. Die Audiospur liegt meist im rohen PCM-Format oder komprimiert als MP3-Datei vor.
Videodaten hingegen verbrauchen massenhaft Speicher, weil pro Sekunde 30 Bilddateien anfallen. Daher spielt das verwendete Kodierungsverfahren, der so genannte Codec, eine entscheidende Rolle, denn ein guter Codec kann die Daten extrem komprimieren, ohne die Bildqualität allzu sehr in Mitleidenschaft zu ziehen. Codecs gibt es wie Sand am Meer, viele davon sind patentiert.
Obwohl ein AVI-Container verschieden kodierte Video- und Audiodaten aufnehmen kann, darf das Kodierungsverfahren nicht mittendrin wechseln. Um also ein Vorspannschnipsel und ein Video hintereinanderzuhängen, muss der Cutter dafür sorgen, dass beide von Anfang an die gleichen Codecs verwenden, oder aber die unterschiedlich kodierten Daten am Ende in ein gemeinsames Ausgabeformat transformieren.
Kameras im Vergleich
Abbildung 1 zeigt die mit dem Programm in Listing 1 ausgelesenen Metadaten zweier Videos. Es nutzt das Modul Video::FrameGrab vom CPAN, dessen »meta()«-Methode Kenndaten eines Videos einholt und in einem Hash ablegt. Abbildung 1 vergleicht die Metadaten der beiden Videos »coolpix.avi« und »camcorder.avi«. Ersteres ist mit einer kleinen Westentaschenkamera, einer Nikon Coolpix S52, aufgenommen, das zweite mit einem digitalen Camcorder der Marke Canon Elura 100. Beide zeichnen das Video mit etwa 30 Frames pro Sekunde auf (»video_fps«), aber der Canon-Recorder nutzt den Codec »ffdv« und die Nikon »ffmjpeg«.
|
Listing 1: |
|---|
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"; |
Auch die Audiodaten speichern beide Kameras unterschiedlich. Während der Camcorder zwei Kanäle aufnimmt, kann die Nikon nur Mono. Zudem ist die Audioqualität unterschiedlich, denn der Camcorder nimmt Audio mit 32000 Messpunkten pro Sekunde auf (Feld »audio_rate«), die Nikon gibt sich mit nur 8000 Messpunkten zufrieden.
Abbildung 1 zeigt auch, dass die »audio_rate« von »8000« bei der Nikon einem Wert von »64000« für die »audio_bitrate« in Bit pro Sekunde gegenübersteht. Beim Camcorder hingegen entstehen pro Audio-Messpunkt 32 Bit (1024000 geteilt durch 32000). Pro Kanal sind das 2 Byte Sample Size.
Ein stummes Titelschnipsel könnte also bei unbekannter Kamera nicht ohne Umwandlung vor einem Video stehen. Zum Glück bieten die Tools Mencoder und Sox [2] die nötigen Funktionen, um die Formate so hinzubiegen, dass Titel und Video trotzdem vereint im AVI-Container liegen können.
Mencoder für Normalos
User, die zum ersten Mal auf Mencoder-Kommandos stoßen, wenden sich meist gleich entsetzt ab, denn scheinbar benötigen selbst einfachste Funktionen 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 wieder die Ausgabedatei folgt:
mencoder input.avi Optionen -o output.avi
Ein Videostrom aus mehreren Eingangsdateien »input1.avi, input2.avi, …« lässt sich ebenfalls sehr einfach erzeugen, indem der Benutzer die Dateinamen der Reihe nach auf der Kommandozeile 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). Wer den Audio-Track stattdessen umkodieren möchte, schreibt »-oac pcm« für das PCM-Format oder »-oac mp3lame« für das mit dem Programm »lame« erzeugte MP3-Format. Braucht der verwendete Encoder noch Optionen wie zum Beispiel »vbr 3«, hängt der Benutzer sie im Mencoder-Aufruf unter der Option »-lameopts« an:
-oac mp3lame -lameopts vbr=3
Entsprechendes gilt für den Videoteil einer AVI-Datei. Um das Videoformat eins zu eins zu übernehmen, taugt »-ovc copy« (v für Video). Um das Videoformat in das Mjpeg-Format umzukodieren und dem verwendeten Encoder die Option »vcodec=mjpeg« mitzugeben, schreibt man »-ovc lavc -lavcopts vcodec=mjpeg«.
Perl automatisiert
Der Vorspann-Generator in Listing 2 nimmt drei Parameter entgegen: Die Videodatei, die er mit einem Titel versieht, und zwei Strings, die er als erste und zweite Zeile im Titelvideo unterbringt. Ruft man ihn mit
video-title-add testvideo.avi "Der Geek" U "Aufzucht und Hege"
|
Listing 2: |
|---|
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 $raten";
149 my $samples = $secs * $rate;
150
151 for (my $i = 0; ($i < $samples); $i++) {
152 print $fh $i / $rate, "t0n";
153 }
154 close $fh;
155
156 shell "sox", $tempfile, "-r", $rate,
157 "-u", "-$sample_size", "-c",
158 $channels, $outfile;
159 }
|
auf, erzeugt er eine neue AVI-Datei »testvideo-withtitle.avi«, die vor dem eigentlichen Video ein 2 Sekunden langes Titelfilmchen (Abbildung 2) enthält.
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 30 Frames pro Sekunde genau 60 Bilder, also setzt das Hauptprogramm »$n« auf »60«.
Mit dem CPAN-Modul Imager erzeugt das Skript zunächst ein neues Imager-Bildobjekt mit den Maßen »$w« mal »$h«. Die gewünschte Farbe Schwarz definiert es über ein Objekt der Klasse Imager::Color. Der in der Variablen »$FONT_FILENAME« gespeicherte Pfad zeigt zu einer TTF-Datei mit dem gewünschten Font. Das neu erzeugte Font-Objekt bietet die Methode »align()« an, die einen ihr übergebenen String an einer definierten Stelle ins Bild malt.
Das mittels »write()« geschriebene Jpeg-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 es nur eine gibt. Der verwendete Codec ist »mjpeg«, da die kleine Nikon ihn nutzt und die Qualität des zusammengeleimten Videos leidet, falls man eine verlustreiche Kodierung in eine ebenfalls verlustreiche andere überführt.
Stille erzeugen
Das in Zeile 43 mit Mencoder geschriebene Titelvideo besitzt keine Tonspur, denn Jpeg-Bildern ist kein Audiosignal zugeordnet und Mencoder wurde mit »-noaudio« ruhiggestellt. Ein Video ohne Ton lässt sich aber nicht mit einem mit Tonspur zusammenschweißen, also muss das Skript nun eine Sounddatei mit 2 Sekunden Stille produzieren.
Ruhe montieren
Die ab Zeile 139 definierte Funktion »silent.wav()« nimmt dazu die Anzahl der Sekunden, den Namen der Ergebnisdatei, die Anzahl der Messpunkte pro Sekunde, die Anzahl der Kanäle und die Byte-Breite eines Messpunkts entgegen. In einer neu angelegten temporären Datei mit dem Suffix ».dat« legt es die Rohdaten ab. Das Utility Sox greift sie sich in Zeile 156 und macht daraus die WAV-Datei.
Zurück im Hauptprogramm wäre es nun eigentlich folgerichtig, die Sounddatei mit dem Titelvideo zu verbandeln. Doch leider schafft Mencoder dies nicht, ohne die Audiotracks auf übelste Art und Weise zu verschieben, was die Synchronisation von Ton und Bild stört. Hingegen funktioniert tadellos, die Audiospur des Originalvideos zu extrahieren, sie mit dem vorher erzeugten Still-Audio zusammenzuschweißen und die entstehende Gesamt-Audiospur mit den zwei aneinandergereihten tonlosen Videos zu verschmelzen.
Der Mplayer-Aufruf in Zeile 57 extrahiert die Audiospur des Originalvideos. Die Zeile 60 legt die stille Tonspur davor und erzeugt so die Gesamt-Tonspur in der Datei »$audio_total«. Zeile 63 wirft Mencoder an, hängt »$video_title« und »$video_file« noch mit der Option »-nosound« hintereinander und konvertiert das Ergebnis in eine AVI-Datei.
Eigentlich sollte man meinen, dass Mencoder die aus den Jpeg-Fotos erzeugte Videodatei im Mjpeg-Format an ein mit einer Kamera im Mjpeg-Format erzeugtes Video anhängen könnte, ohne am Codec herumzufummeln, aber Mencoder bricht mit einer Fehlermeldung ab. Falls Mencoder die Camcorderdatei aber selbst in Mjpeg umwandelt, funktioniert das spätere Anhängen tadellos. Das ist 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.
Nun fehlt noch, die vorher angelegte Gesamt-Tonspur »$audio_total« in das eben erzeugte, noch tonlose Gesamt-Video einzubinden. Der Mencoder-Aufruf in Zeile 68 mit der Option »-audiofile« erledigt genau dies, 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 Endung auf das Format. Die temporären Dateien verwaltet das CPAN-Modul File::Temp.
Die Funktion »shell()«, definiert ab Zeile 85, führt ein als Liste übergebenes Shellkommando mit Parametern aus, prüft, ob alles klarging, und bricht das Programm ab, falls etwas schieflief. 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 beiden Tools Mencoder und Sox sind auf Linux-Systemen oft schon installiert. 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 ist auf jeden Fall so zu installieren. Der in Zeile 13 definierte Pfad zur Truetype-Fontdatei für den verwendeten Font »VeraSe.ttf« ist unter Umständen an die lokalen Gegebenheiten der Distro anzupassen.
Abspann ähnlich
Neben einem Titel trägt auch ein Abspann zum Wert eines Videos bei. Hierzu erweitert der Künstler einfach das Skript und lässt 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, U "audiodump.wav", $audio_trailer, "-o", U $audio_total;
Das aus Jpeg-Bildern genauso wie »$video_title« erzeugte stille Abspannvideo hängt er dann hinter den Parameter »$video_file« in den Mencoder-Aufruf von Zeile 63 ein. Der Kamermann freut sich bestimmt über die Erwähnung seines Namens in den so genannten Credits und passende Links ins Web verweisen Interessierte Betrachter auf weiterführende Informationen. (jcb)
|
Infos |
|---|
|
[1] Mencoder: [http://www.mplayerhq.hu] [2] Sox: [http://sox.sourceforge.net] [3] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2010/01/Perl] |
|
Der Autor |
|---|
|
Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. |








