Ein Perlskript mit Gtk2-Oberfläche merkt sich, wie weit gespeicherte Videos abgespult wurden und fährt auf Wunsch bei der letzten Unterbrechung fort.
Wer wie ich im Bus auf dem Weg zur Arbeit auf dem Netbook Fernsehfilme ansieht, muss unter Umständen an der spannendsten Stelle aussteigen und das Video anhalten. Bleiben dann beim nächsten Teilstück nur 15 Minuten, möchte man vielleicht nicht wieder ein weiteres Bruchstück des Filmes verfolgen, sondern lieber die ebenfalls heruntergeladene Tagesschau ([2]) abspielen. Um die angefangenen Sendungen später fertigzusehen, klickt man sie später einfach wieder auf der graphischen Oberfläche in Abbildung 1 an und das Programm fährt genau dort fort, wo der User vorher den Lauf abgebrochen hat.
Abbildung 1: Die Gtk2-Oberfläche startet auf Mausdruck ausgewählte Videos an der Stelle der letzten Unterbrechung. |
Eine Zeitmaschine für gespeicherte Videos also, genau so, wie das mein Tivo [3] seit mehr als zehn Jahren zuhause in San Francisco macht. Der digitale Videorekorder speichert eine Reihe von Fernsehsendungen und dazu jeweils den Zeitstempel der letzten Unterbrechung. Wählt man einen Film aus der Liste aus, nimmt der Tivo den Abspielvorgang genau dort wieder auf.
iTunes oder Podcast-Software machen es ähnlich. Wie schwierig wäre es
wohl, ein kurzes Skript eigenhändig in Perl zu schreiben? Listing ttv
zeigt
mit weniger als 150 Zeilen das Ergebnis. Nun wäre es natürlich vermessen,
ein Wunderwerk der Videotechnik wie mplayer
nachzubauen. Außerdem bringt
der Tausendsassa auch noch alle Voraussetzungen für eine
Zeitmaschinensteuerung mit: Wie Abbildung 2 zeigt, zählt der laufende
mplayer
stetig die laufenden Videosekunden hoch, es ist also für ein
Steuerprogramm relativ einfach, festzustellen, wie weit der Player
mittlerweile mit dem Abspielen fortgeschritten ist. Das muss nicht immer
linear geschehen, denn der User darf zum Beispiel mit den
PageUp/PageDown-Tasten während des Laufs wild im Video hin- und herspringen.
Als zweite Vorraussetzung für die Fernsteuerung durch ein externes Programm
wie das vorgestellte Perlskript ist die Option -ss N
, die den Player
mit einer angefügten Sekundenzahl anweist, das Video nicht von Anfang an
abzuspielen, sondern die ersten N Sekunden zu überspringen.
Abbildung 2: Mplayer zählt während des Laufs die Sekunden des Videos auf der Standardausgabe hoch. Das ttv-Skript greift die Daten von dort ab. |
Damit ist klar, wie ttv
funktioniert: Die Gtk2-getriebene
Benutzeroberfläche wartet darauf, dass der User ein Video doppelklickt.
Ist es das erste Mal, wirft die GUI den mplayer
an und lässt ihn das Video
von Anfang an abspielen. Während der Player läuft und der User den
laufenden Film genießt, greift das Skript über Mplayers Standardausgabe die
verstrichenen Videosekunden ab und speichert diese laufend zwischen. Bricht
der User den Abspielvorgang ab (z.B. indem er im mplayer-Fenster die Taste
``q'' drückt), tritt die graphische Oberfläche wieder vor den User und
das Skript legt die Abspieldauer unter dem Namen des Videos in der
YAML-Datei ttv.dat
unter dem Home-Verzeichnis ab (Abbildung 3).
Abbildung 3: In der YAML ~/.ttv.dat legt das Skript die Spieldauer tatsächlich angespielter Videos fest. Hier nicht gespeicherte Videos spielt es von Anfang an ab, falls der User sie auswählt. |
Erfahrene Snapshot-Leser ahnen schon, dass die Steuerung der GUI-Komponenten
im ruckelfreien Zusammenspiel mit einem extern aufgerufenen Programm wie
mplayer
wieder einmal mit dem Event-basierten Perl-Framework POE
vom CPAN realisiert wurde. Wie auch mit Terminalsteuerungen auf
Curses-Basis hopst POE mit allen erdenklichen GUI-Eventschleifen im
Takt und eignet sich hervorragend dazu, quasi-parallele Prozesse zu
steuern. Im vorliegenden Fall startet das GUI-Skript den Videospieler
hinter den Kulissen tatsächlich als separaten Prozess, aber das Abgreifen
von dessen Ausgabedaten, das Anspringen von Callback-Handlern
und die Kontrolle darüber, ob der Player überhaupt noch läuft oder
vom User schon abgeschaltet wurde, verwaltet POE äußerst
robust und elegant im Single-Process, Single-Thread-Verfahren.
Da Zeile 7 von ttv
das Modul Gtk2 vor den POE-Modulen anfordert, ist POE
darüber informiert, dass nicht seine eigene Eventschleife den Prozess
steuern wird, sondern Gtk2 mit dem CPAN-Modul POE::Loop::Glib als
unsichtbarer Brücke. Zeile 15 legt den Pfad der YAML-Datei fest, in der
das Skript später den Hash-Inhalt der Referenz $OFFSETS
speichert.
Die Datenstruktur weist
Videodateinamen jeweils einer Fließkommazahl zu, die die bereits
abgespielten Sekunden angibt.
Die globale Variable $REWIND gibt an, dass das Skript jeweils 10 Sekunden zurückspult, bevor es wieder mitten in ein bereits zu einem früheren Zeitpunkt unterbrochenes Videos hineinspringt. Das gibt dem Zuschauer Gelegenheit, im Ablauf schnell wieder Fuß zu fassen. Verfügbare Videos sucht Zeile 19 im aktuellen Verzeichnis als .mp4 und .avi-Dateien zusammen und muss unter Umständen angepasst werden, falls der User andere Formate bevorzugt.
POE-typisch definiert der Session-Konstruktor ab Zeile 26 insgesamt fünf verschiedene Zustände, zwischen denen der im Skript definierte Automat hin- und herspringt. Nach dem Starten des POE-Kernels in Zeile 35 läuft die GUI und arbeitet User-Eingaben ab, bis der User das Programm mit einem Klick auf das Schließ-Icon des Hauptfensters beendet.
Per Definition legt der ``_start''-Zustand in Zeile 28 den
Anfangszustand fest. Die ihm zugewiesene Funktion ui_start
baut die graphische Oberfläche auf und ist ab Zeile 62 definiert.
Wie schon in früheren Beiträgen zum Thema POE ausgeführt (z.B. [4]),
holen die Makros KERNEL
, SESSION
und HEAP
Automatenvariablen
aus Perls Array @_ für Funktionsargumente. HEAP
ist ein
Hash für eine ``Session'' des Automaten und dient zum Ablegen
allerlei globaler Variablen, die der Automat dann von Callback zu
Callback durchschleift, allerdings sauber von anderen Sessions getrennt.
Zeile 68 ruft den Konstruktor des GUI-Hauptfensters Gtk2::Window auf, in
dessen Rahmen später eine Listbox mit abspielbereiten Videos zu
liegen kommt. Der Parameter 'toplevel' gibt an, dass es sich um
das Hauptfenster der Applikation handelt. Eine Referenz darauf legt
das Skript im HEAP ab, nicht, um später in Callbacks darauf zuzugreifen,
sondern um sicher zu stellen, dass Perl eine Referenz auf das Hauptfenster
in einer Variable speichert, die sich nicht mit Abschluss der Funktion
ui_start
in Luft auflöst. Geschähe dies nämlich, fiele das
Applikationsfenster dann sang- und klanglos in sich zusammen, obwohl
die Applikation noch weiterlaufen soll.
Der Aufruf der Methode
signal_ui_destroy
defininiert in Zeile 70, dass mit dem Zusammenfallen
des Hauptfensters (zum Beispiel weil der User mit der Maus das
Schließ-Icon angeklickt hat) sich auch die Applikation, also der POE-Kernel
beendet. Das in Zeile 73 erzeugte Widget Gtk2::SimpleList speichert
die Daten der zweispaltigen Anzeige. Wie Abbildung 1 zeigt, besteht
jede Zeile der Video-Liste links aus einem Zeitstempel und rechts
aus dem Namen einer Videodatei. Beide Spalten sind vom Typ ``text'',
beherbergen also einfache Zeichenketten ohne farbliche Hervorhebungen
oder anderen Schnickschnack. Unter dem Kürzel slist
speichert
das Skript eine Referenz auf das Widget im Session-Heap ab.
Die Methode add()
fügt die Listbox ins Hauptfenster ein und
das nachfolgende show_all()
zeichnet die GUI auf den Bildschirm.
Die ab Zeile 90 definierte Funktion listbox_redraw()
frischt die
Listbox auf, indem sie ihr einfach unter dem Eintrag data
neue
Werte in einem Array von zweielementigen Arrays unterschiebt. Schwarze
Magie im Widget (eine mit tie
gebundene Datenstruktur) löst ohne
weitere Maßnahmen dann sofort ein Neuzeichnen der graphischen Darstellung
aus. Die Funktion timer()
ab Zeile 100 bringt den Zeitstempel einer
Videodatei, der in Sekunden vorliegt, im Format hh:mm:ss auf Vordermann.
Falls der User eine Listbox-Zeile mit der Maus doppelklickt, sorgt
der Aufruf von signal_connect
in Zeile 80 dafür, dass der
POE-Zustandsautomat den Zustand ``click'' anspringt, und damit die
ab Zeile 48 definierte Funktion click()
aufruft. Als einziges
Argument übergibt er ihr in ARG0 eine Referenz auf Listbox-Zustandsdaten,
aus denen die Funktion get_row_data_from_path()
diejenige
Zeile hervorzaubert, auf die der User geklickt hat. Das zweite Element
der zurückkommenden Arrayreferenz ist der Dateiname des gewünschten
Videos. Der Aufruf yield()
weist den POE-Kernel in Zeile 57 an,
den Zustand ``play_video'' anzuspringen und ihm den Dateinamen des
abzuspielenden Videos zu überreichen.
Dies startet die Funktion play_video
ab Zeile 114, die zunächst
herausfindet, ob in der globalen Variablen $OFFSETS ein Sekundenwert
für das Video vorliegt und dann über das Modul POE::Wheel::Run
den externen mplayer startet. Programm und Argumente nimmt das Wheel,
ein Rädchen im Getriebe des POE-Kernels, getrennt als Program
und ProgramArgs
entgegen. Die Option -fs
startet mplayer im
Fullscreenmodus für vollen Videogenuss und -ss
gibt die Anzahl
der Sekunden vor, in die mplayer in das Video hineinspringt, bevor
er mit dem Abspielen beginnt.
Da der mplayer die Ausgabe der Videosekunden nicht durch Zeilenumbrüche trennt, greift der normale zeilenbasierte Filter von POE::Wheel::Run nicht und POE::Filter::Stream kommt in Zeile 131 zum Einsatz. Er wartet nicht, bis eine Zeile vollständig vorliegt, sondern lässt das Wheel den Ausgabezustand ``output'' anspringen, sobald ein neues Textschnipsel vorliegt.
Die in diesem Fall aufgerufene
Funktion stdout_handler()
ab Zeile 144 erhält so immer ein
Schnipsel neu aufgeschnappter mplayer-Diagnoseausgabe und
versucht mit dem regulären Ausdruck in Zeile 148, die in Abbildung 2 rot
eingefärbten Videosekunden daraus zu extrahieren.
Hierzu sucht es die Zeichenkette ``V:'' entweder am Zeilenanfang oder
nach einem Leerzeichen und fängt eine nachfolgende Fließkommazahl in
einer Capture-Klammer ein. Der gefundene Wert steht anschließend in der
Variablen $1. Die erste Klammer im Regex dient nur der Gruppierung
von regulären Ausdrücken und hat keine Capture-Funktion, was
die Anweisung ``?:'' zum Ausdruck bringt.
Findet der Regex
einen passenden Wert, legt stdout_handler()
ihn unter dem
Videonamen im globalen
Hash ab, auf den die Referenz $OFFSETS zeigt.
Diese Daten sichert das
Skript jeweils am Ende des Abspielvorgangs in der YAML-Datei, wenn
es den CloseEvent ``play_ended'' und damit die Funktion play_ended()
ab Zeile 39 anspringt.
Die Kernel-Methode sig_child()
in Zeile 137 weist den POE-Kernel
an, den soeben gestarteten und später eventuell herumlungernden Fremdprozess
mit dem mplayer
abzuschießen, falls das Programm abbricht.
Das CPAN-Modul POE::Loop::Glib wies zur Fertigstellung des Artikels noch einen Fehler in Version 0.037 auf, der die GUI nach einigen Sekunden Video abstürzen lässt. Falls beim Erscheinen des Beitrags die Version 0.038 auf dem CPAN verfügbar ist, hat der Modulautor meinen Patch hoffentlich eingespielt. Falls nicht, steht er unter den Listings auf dem Server des Linux-Magazins zum Download bereit. Folgende Befehlsfolge bringt die Moduldistribution nach dem Download des Tarballs von search.cpan.org auf den neuesten Stand:
$ tar zxfv POE-Loop-Glib-0.037.tgz $ cd POE-Loop-Glib-0.037 $ patch -p1 <../poe-loop-glib-0.037.patch patching file Changes patching file Makefile.PL patching file lib/POE/Loop/Glib.pm
und das übliche ``perl Makefile.PL; make; sudo make install'' installiert das gepatchte Modul im Perlbaum. Alle weiteren Module werden entweder mit einer CPAN-Shell installiert oder mit dem Package-Manager der verwendeten Linux-Distribution, falls diese die entsprechenden Module als Packages führt. Es ist darauf zu achten, dass das CPAN-Modul POE::Loop::Glib als unsichtbare Brücke ebenfalls installiert werden muss, auch wenn es nicht explizit im Listing erscheint.
Es bleibt anzumerken, dass man den angebotenen Funktionsumfang des Skripts nicht über Gebühr ausreizen sollte. Ich rate davon ab, mehr als drei Spielfilme gleichzeitig in Angriff zu nehmen, sonst kann es beim unaufmerksamen Zuschauer zu lustigen Verwirrungen führen, besonders wenn Matt Daemon und Leonardo DeCaprio in ähnlichen Kinowerken spielen.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Gtk2 '-init'; 004 use Gtk2::SimpleList; 005 use POE; 006 use POE::Wheel::Run; 007 use POE::Filter::Stream; 008 use YAML qw(LoadFile DumpFile); 009 010 my ($home) = glob "~"; 011 my $YAML_FILE = "$home/.ttv.dat"; 012 my $OFFSETS = {}; 013 my $REWIND = 10; 014 015 my @VIDEOS = sort { -M $a <=> -M $b } 016 (<*.mp4>, <*.avi>); 017 018 if(-f $YAML_FILE) { 019 $OFFSETS = LoadFile( $YAML_FILE ); 020 } 021 022 POE::Session->create( 023 inline_states => { 024 _start => \&ui_start, 025 play_video => \&play_video, 026 click => \&click, 027 output => \&stdout_handler, 028 play_ended => \&play_ended, 029 }); 030 031 $poe_kernel->run(); 032 exit 0; 033 034 ########################################### 035 sub play_ended { 036 ########################################### 037 my($kernel, $heap) = @_[KERNEL, HEAP]; 038 039 DumpFile( $YAML_FILE, $OFFSETS ); 040 listbox_redraw($heap->{slist}); 041 } 042 043 ########################################### 044 sub click { 045 ########################################### 046 my($kernel, $session, $gtk_list_data) = 047 @_[KERNEL, SESSION, ARG1]; 048 049 my ($sl, $path) = @$gtk_list_data; 050 my $row_ref = 051 $sl->get_row_data_from_path($path); 052 053 $kernel->yield("play_video", 054 $row_ref->[1]); 055 } 056 057 ########################################### 058 sub ui_start { 059 ########################################### 060 my ($kernel, $session, $heap) = 061 @_[KERNEL, SESSION, HEAP]; 062 063 $heap->{main_window} = 064 Gtk2::Window->new ('toplevel'); 065 066 $kernel->signal_ui_destroy( 067 $heap->{main_window}); 068 069 $heap->{slist} = Gtk2::SimpleList->new ( 070 'Timer' => 'text', 071 'Video' => 'text', 072 ); 073 074 listbox_redraw( $heap->{slist} ); 075 076 $heap->{slist}->signal_connect( 077 row_activated => 078 $session->callback("click")); 079 080 $heap->{main_window}->add( 081 $heap->{slist}); 082 $heap->{main_window}->show_all; 083 } 084 085 ########################################### 086 sub listbox_redraw { 087 ########################################### 088 my($slist) = @_; 089 090 @{$slist->{data}} = ( 091 map { [ timer($_), $_ ] } @VIDEOS 092 ); 093 } 094 095 ########################################### 096 sub timer { 097 ########################################### 098 my($video) = @_; 099 100 my $sec = 0; 101 $sec = $OFFSETS->{$video} if 102 exists $OFFSETS->{$video}; 103 104 return sprintf("%02d:%02d:%02d", 105 int($sec/(60*60)), 106 ($sec/60)%60, $sec%60); 107 } 108 109 ########################################### 110 sub play_video { 111 ########################################### 112 my ($kernel, $session, $heap, $video) = 113 @_[KERNEL, SESSION, HEAP, ARG0]; 114 115 my $offset = 0; 116 117 $offset = $OFFSETS->{ $video } - $REWIND 118 if exists $OFFSETS->{ $video } 119 and $OFFSETS->{ $video } > $REWIND; 120 121 my $wheel = 122 POE::Wheel::Run->new( 123 Program => "/usr/bin/mplayer", 124 ProgramArgs => 125 ["-fs", "-ss", $offset, $video], 126 StdoutFilter => 127 POE::Filter::Stream->new(), 128 StdoutEvent => 'output', 129 CloseEvent => 'play_ended', 130 ); 131 132 $heap->{video} = $video; 133 $kernel->sig_child( $wheel->PID(), 134 'sig_child' ); 135 136 $heap->{player} = $wheel; 137 } 138 139 ########################################### 140 sub stdout_handler { 141 ########################################### 142 my ($heap, $input) = @_[HEAP, ARG0]; 143 144 if($input =~ /(?:^| )V:\s*([\d.]+)/m) { 145 $OFFSETS->{$heap->{video}} = $1; 146 } 147 }
[Anmerkung für die Redaktion: Bitte nicht vergessen, den angehängten Patch poe-loop-glib-0.037.patch mit auf den Listing-Server zu stellen].
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/05/Perl
Tagesschau-Download in verschiedenen Formaten: http://www.tagesschau.de/export/video-podcast/webl/tagesschau
Tivo, der digitale Videorekorder, http://tivo.com
``Verkehrte Welt'', Michael Schilli, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2010/03/tleW-etrhekreV
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. |