Aus Linux-Magazin 01/2010

Perl-Skript am Schneidetisch

© dieterwald, Pixelio.de

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:
»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";
Abbildung 1: Metadaten zweier Videos, oben mit Nikon Coolpix S52, unten mit Canon Elura 100.

Abbildung 1: Metadaten zweier Videos, oben mit Nikon Coolpix S52, unten mit Canon Elura 100.

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:
»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 $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.

Abbildung 2: Der per Skript erzeugte Videovorspann läuft im Mplayer vor dem eigentlichen Video.

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

Abbildung 3: Die Rohdaten einer 2 Sekunden langen stillen Audiodatei.

Abbildung 3: Die Rohdaten einer 2 Sekunden langen stillen Audiodatei.

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.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben