Video mit Köpfchen (Linux-Magazin, Januar 2010)

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.

Film aus Einzelbildern

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.

Was ist ein Codec?

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.

Kameras im Vergleich

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.

Listing 1: video-meta

    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";

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

Perl automatisiert

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.

Listing 2: video-title-add

    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 }

Stille erzeugen

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.

Ra-ru-rick, Schillitrick

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!

Ton für den Stummfilm

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.

Installation

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.

Abspann ähnlich

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.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/01/Perl

Michael Schilli

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