Aus Linux-Magazin 05/2010

Perl-Skript steuert Videowiedergabe

© Martin Wendring, Pixelio.de

Ein Perl-Skript mit GTK-2-Oberfläche merkt sich, wie weit sein Anwender gespeicherte Videos angesehen hat, und fährt auf Wunsch an der Stelle der letzten Unterbrechung wieder fort .

Wer wie ich im Bus auf dem Weg zur Arbeit mit dem Netbook Fernsehfilme ansieht, der muss mit an Sicherheit grenzender Wahrscheinlichkeit genau an der spannendsten Stelle umsteigen. Bleiben auf dem nächsten Teilstück nur 15 Minuten, dann lohnt es sich vielleicht nicht, nur eine weitere Szene des Films zu verfolgen. Stattdessen sehe ich mir dann auf der Kurzstrecke lieber die ebenfalls aufgezeichnete Tagesschau [2] an. Um den angefangenen Film aber später doch fertig zu sehen, genügt ein Klick (siehe Abbildung 1) und das Programm fährt genau dort fort, wo der User den Lauf vorher gestoppt hatte.

Das Durcheinander verhindern

Eine Zeitmaschine für gespeicherte Videos also, genau so, wie das mein Tivo [3] seit mehr als zehn Jahren zu Hause in San Francisco macht. Der digitale Videorekorder speichert eine Reihe von Fernsehsendungen und dazu jeweils den Zeitstempel der letzten Unterbrechung. Wählt man später wieder einen dieser Filme aus der Liste, nimmt der Recorder den Abspielvorgang genau dort wieder auf. I-Tunes oder Podcast-Software machen es ähnlich. Wie schwierig wäre es wohl, ein kurzes Skript mit derselben Funktion eigenhändig in Perl zu schreiben? Listing 1 zeigt mit weniger als 150 Zeilen das Ergebnis.

Abbildung 1: Die GTK-2-Oberfläche startet auf Mausdruck ausgewählte Videos an der Stelle der letzten Unterbrechung - beziehungsweise beim ersten Mal am Anfang.

Abbildung 1: Die GTK-2-Oberfläche startet auf Mausdruck ausgewählte Videos an der Stelle der letzten Unterbrechung – beziehungsweise beim ersten Mal am Anfang.

Listing 1:

#!/usr/local/bin/perl -w
use strict;
use Gtk2 '-init';
use Gtk2::SimpleList;
use POE;
use POE::Wheel::Run;
use POE::Filter::Stream;
use YAML qw(LoadFile DumpFile);
 
my  ($home)    = glob "~";
my  $YAML_FILE = "$home/.ttv.dat";
my  $OFFSETS   = {};
my  $REWIND    = 10;

my @VIDEOS  = sort { -M $a <=> -M $b } 
              (<*.mp4>, <*.avi>);

if(-f $YAML_FILE) {
  $OFFSETS = LoadFile( $YAML_FILE );
}

POE::Session->create(
  inline_states => {
    _start     => \&ui_start,
    play_video => \&play_video,
    click      => \&click,
    output     => \&stdout_handler,
    play_ended => \&play_ended,
});

$poe_kernel->run();
exit 0;

###########################################
sub play_ended {
###########################################
  my($kernel, $heap) = @_[KERNEL, HEAP];

  DumpFile( $YAML_FILE, $OFFSETS );
  listbox_redraw($heap->{slist});
}

###########################################
sub click {
###########################################
  my($kernel, $session, $gtk_list_data) =
                 @_[KERNEL, SESSION, ARG1];

  my ($sl, $path) = @$gtk_list_data;
  my $row_ref = 
      $sl->get_row_data_from_path($path);

    $kernel->yield("play_video", 
                   $row_ref->[1]);
}

###########################################
sub ui_start {
###########################################
  my ($kernel, $session, $heap) = 
                 @_[KERNEL, SESSION, HEAP];

  $heap->{main_window} = 
            Gtk2::Window->new ('toplevel');

  $kernel->signal_ui_destroy(
                $heap->{main_window});

  $heap->{slist} = Gtk2::SimpleList->new (
    'Timer'    => 'text',
    'Video'    => 'text',
  );

  listbox_redraw( $heap->{slist} );

  $heap->{slist}->signal_connect(
      row_activated => 
          $session->callback("click"));

  $heap->{main_window}->add(
                         $heap->{slist});
  $heap->{main_window}->show_all;
}

###########################################
sub listbox_redraw {
###########################################
    my($slist) = @_;

    @{$slist->{data}} = (
       map { [ timer($_), $_ ] } @VIDEOS
    );
}

###########################################
sub timer {
###########################################
    my($video) = @_;

    my $sec = 0;
    $sec = $OFFSETS->{$video} if 
                exists  $OFFSETS->{$video};

    return sprintf("%02d:%02d:%02d", 
        int($sec/(60*60)), 
        ($sec/60)%60, $sec%60);
}

###########################################
sub play_video {
###########################################
  my ($kernel, $session, $heap, $video) =
           @_[KERNEL, SESSION, HEAP, ARG0];

  my $offset = 0;

  $offset = $OFFSETS->{ $video } - $REWIND 
    if exists $OFFSETS->{ $video } 
       and $OFFSETS->{ $video } > $REWIND;

  my $wheel =
    POE::Wheel::Run->new(
      Program     => "/usr/bin/mplayer",
      ProgramArgs => 
          ["-fs", "-ss", $offset, $video],
      StdoutFilter => 
               POE::Filter::Stream->new(),
      StdoutEvent => 'output',
      CloseEvent  => 'play_ended',
  );

  $heap->{video} = $video;
  $kernel->sig_child( $wheel->PID(), 
                      'sig_child' );

  $heap->{player} = $wheel;
}

###########################################
sub stdout_handler {
###########################################
    my ($heap, $input) = @_[HEAP, ARG0];

    if($input =~ /(?:^| )V:s*([d.]+)/m) {
        $OFFSETS->{$heap->{video}} = $1;
    }
}

Nun wäre es vermessen, ein Wunderwerk der Videotechnik wie Mplayer nachzubauen. Doch bringt der Tausendsassa ja glücklicherweise bereits alle Voraussetzungen für eine Zeitmaschinensteuerung mit: Wie Abbildung 2 zeigt, zählt der laufende Mplayer stetig die verstrichenen Videosekunden hoch, es ist also für ein Steuerprogramm relativ einfach festzustellen, wie weit der Player in einem Film fortgeschritten ist. Dabei darf der User sogar mit den [Page up]- und [Page down]-Tasten während der Vorführung wild im Video hin und her springen. Eine zweite Voraussetzung für die Fernsteuerung durch ein externes Programm wie das vorgestellte Perl-Skript 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. Damit ist auch schon klar, wie »ttv« funktioniert: Die GTK-2-getriebene Benutzeroberfläche wartet darauf, dass der User ein Video doppelklickt. Ist es das erste Mal, wirft das GUI den Mplayer an und lässt ihn das Video von Anfang an abspielen. Während der Player arbeitet und der User den laufenden Film genießt, greift das Skript über Mplayers Standardausgabe die verstreichenden Videosekunden ab und speichert diese laufend zwischen.

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.

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.

Bricht der User den Abspielvorgang ab (zum Beispiel indem er im Mplayer-Fenster die Taste [Q] drückt), taucht die grafische Oberfläche wieder auf und das Skript legt die Abspieldauer unter dem Namen des Videos in der YAML-Datei »~/.ttv.dat« im Homeverzeichnis ab (Abbildung 3).

Abbildung 3: In der YAML-Datei »~/ttv.dat« legt das Skript die Spieldauer angespielter Videos fest. Hier nicht gespeicherte Videos spielt es von Anfang an ab, falls der User sie auswählt.

Abbildung 3: In der YAML-Datei »~/ttv.dat« legt das Skript die Spieldauer angespielter Videos fest. Hier nicht gespeicherte Videos spielt es von Anfang an ab, falls der User sie auswählt.

Tanz auf fremden Hochzeiten

Erfahrene Snapshot-Leser ahnen schon, dass ich 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 realisiere. Wie auch bei Terminalsteuerungen auf Curses-Basis hopst POE mit allen erdenklichen GUI-Eventschleifen im Takt und eignet sich hervorragend dazu, quasi-parallele Prozesse zu steuern. In diesem Fall startet das GUI-Skript den Videospieler hinter den Kulissen tatsächlich als separaten Prozess, aber im Hintergrund greift POE im robusten und eleganten Single-Process-/Single-Thread-Verfahren dessen Ausgabedaten ab, springt Callback-Handler an und kon- trolliert auch, ob der Player überhaupt noch läuft oder ob der User ihn nicht etwa schon abgeschaltet hat.

Da Zeile 3 von »ttv« das GTK-2-Modul vor den POE-Modulen anfordert, ist POE darüber informiert, dass nicht seine eigene Eventschleife den Prozess steuern wird, sondern GTK 2 mit dem CPAN-Modul POE::Loop::Glib als unsichtbarer Brücke. Zeile 11 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 früher unterbrochenes Videos hineinspringt. Das gibt dem Zuschauer die Ge- legenheit, im Ablauf des Filmgeschehens schnell wieder Fuß zu fassen. Verfügbare Videos sucht Zeile 15 im aktuellen Verzeichnis als MP4- und AVI-Dateien zusammen. Sie ist unter Umständen anzupassen, falls der User andere Formate bevorzugt.

Automat und Zustände

POE-typisch definiert der Session-Konstruktor ab Zeile 22 insgesamt fünf ver- schiedene Zustände, zwischen denen der im Skript definierte Automat hin und her springt. Nach dem Starten des POE-Kernels in Zeile 31 läuft das GUI und arbeitet die User-Eingaben ab, bis der Anwender das Programm endlich mit einem Klick auf das Schließen-Icon des Hauptfensters beendet.

Per Definition legt der »_start«-Zustand in Zeile 24 den Anfangszustand fest. Die ihm zugewiesene Funktion »ui_start()« baut die grafische Oberfläche auf und ist ab Zeile 58 definiert.

Wie schon in früheren Beiträgen zum Thema POE ausgeführt (zum Beispiel in [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 Ab- legen allerlei globaler Variablen, die der Automat dann von Callback zu Callback durchschleift, wobei er sie allerdings sauber von anderen Sessions trennt.

Speichern oder zusammenklappen

Zeile 64 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 sicherzustellen, dass Perl eine Referenz auf das Hauptfenster in einer Vari- ablen speichert, die sich nicht mit dem Abschluss der Funktion »ui_start()« in Luft auflöst. Denn wenn dies geschähe, fiele daraufhin das Applikationsfenster sang- und klanglos in sich zusammen, obwohl die Applikation ja noch weiterlaufen soll.

Der Aufruf der Methode »signal_ui_destroy()« definiert in Zeile 66, dass mit dem Zusammenfallen des Hauptfensters (zum Beispiel weil der User mit der Maus das Schließen-Icon angeklickt hat) sich auch die Applikation, also der POE-Kernel beendet. Das in Zeile 69 erzeugte Widget »Gtk2::SimpleList« nimmt die Daten der zweispaltigen Anzeige auf.

Wie Abbildung 1 zeigt, besteht jede Zeile dieser Videoliste links aus einem Zeitstempel und rechts aus dem Namen einer zugehörigen Videodatei. Beide Spalten sind vom Datentyp Text, beherbergen also nur einfache Zeichenketten ohne farbliche Hervorhebungen oder anderen Schnickschnack.

Unter dem Kürzel »slist« speichert das Skript eine Referenz auf das Widget im Sessionheap ab. Die Methode »add()« fügt die Listbox in das Hauptfenster ein und das nachfolgende »show_all()« zeichnet das GUI dann schließlich auf den Bildschirm.

Schwarze Magie im Widget

Die ab Zeile 86 definierte Funktion »listbox_redraw()« frischt die Listbox auf, indem sie ihr einfach unter dem Eintrag »data« neue Werte in Form eines 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 grafischen Darstellung aus. Die Funktion »timer()« ab Zeile 96 bringt den Zeitstempel einer Videodatei, der in der Einheit Sekunden vorliegt, in dem Format »hh:mm:ss« auf Vordermann.

Falls der User eine Listbox-Zeile mit der Maus doppelklickt, sorgt der Aufruf von »signal_connect« in Zeile 76 dafür, dass der POE-Zustandsautomat den Zustand »click« anspringt und damit die ab Zeile 44 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()« genau jene 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 53 an, den Zustand »play_video« anzuspringen und ihm den Dateinamen des abzuspielenden Videos zu überreichen.

Dies startet die Funktion »play_video()« ab Zeile 110, 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. Das Pro- gramm und die Argumente nimmt das Wheel, ein Rädchen im Getriebe des POE-Kernels, getrennt als »Program« und »ProgramArgs« entgegen. Die Option »-fs« startet Mplayer im Fullscreen-Modus für vollen Videogenuss, »-ss« gibt die Anzahl der Vorlauf-Sekunden vor.

Auf der Suche nach der vergangenen Zeit

Da der Mplayer die Ausgabe der Videosekunden nicht durch Zeilenumbrüche trennt, greift der normale zeilenbasierte Filter von POE::Wheel::Run nicht, also kommt in Zeile 127 POE::Filter::Stream zum Einsatz. Dieser Filter wartet nicht, bis eine Zeile vollständig ist, sondern lässt das Wheel den Ausgabezustand »output« anspringen, sobald ein neues Textschnipsel vorliegt.

Die in diesem Fall aufgerufene Funktion »stdout_handler()« ab Zeile 141 erhält damit immer ein Schnipsel der gerade neu aufgeschnappten Mplayer-Diagnoseausgaben und probiert dann mit dem regulären Ausdruck in Zeile 146, die in Abbildung 2 rot eingefärbten Videosekunden daraus zu extrahieren.

Hierzu sucht es zunächst die Zeichenkette »V:« entweder am Zeilenanfang oder nach einem Leerzeichen und fängt eine nachfolgende Fließkommazahl (»[d.]+)«) in einer Capture-Klammer ein. Der gefundene Wert steht anschließend in der Variablen »$1«, die den Treffer aus der ersten Klammer des regulären Ausdrucks aufnimmt. Diese erste Klammer dient lediglich der Gruppierung und hat selbst keine Capture-Funktion, was die Anweisung »?:« in dem regulären Ausdruck bewirkt.

Findet der Regex nun einen passenden Wert, dann legt »stdout_handler()« ihn unter dem Videonamen im globalen Hash ab, auf den die Referenz »$OFFSETS« zeigt. Diese Daten sichert das Skript jeweils am Schluss jedes Abspielvorgangs in der schon erwähnten YAML-Datei im Homeverzeichnis des Benutzers, wenn es den Close-Event »play_ended« und damit auch die Funktion »play_ended()« ab Zeile 35 anspringt.

Die Kernel-Methode »sig_child()« in der Zeile 134 weist den POE-Kernel umgehend an, den soeben gestarteten und später eventuell immer noch herumlungernden Fremdprozess mit dem Mplayer ohne Warnung abzuschießen, falls das Programm abbricht.

Patch für Modul POE::Loop::Glib

Das CPAN-Modul POE::Loop::Glib wies zur Zeit der Fertigstellung dieses Artikels noch einen schwerwiegenden Fehler in seiner Version 0.037 auf, der das GUI nach einigen Sekunden Video regelmäßig abstürzen ließ. Falls beim Erscheinen dieses Beitrags bereits die Version 0.038 auf dem CPAN verfügbar ist, dann hat der Modulautor mein Patch, das diesen Fehler behebt, hoffentlich schon eingespielt. Sollte dies jedoch wider Erwarten nicht der Fall sein, dann steht das Patch unter den anderen Listings auf dem Server des Linux-Magazins zum Download bereit. Die folgende kurze Befehlsfolge bringt die Moduldistribution nach dem Download des Tarballs von »search.cpan.org« in diesem Fall schnell auf den allerneuesten 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

Das übliche »perl Makefile.PL; make; sudo make install« installiert das gepatchte Modul am Ende im Perl-Verzeichnisbaum. Alle weiteren Module fügt der Anwender entweder mit einer CPAN-Shell oder über den Package-Manager der verwendeten Linux-Distribution hinzu, falls diese die entsprechenden Perl-Module als Packages führt. Es ist in jedem Fall darauf zu achten, dass das CPAN-Modul POE::Loop::Glib als unsichtbare Brücke ebenfalls zu installieren ist, auch wenn es nicht explizit im Listing erscheint. Gebraucht wird es doch. Anzumerken bleibt, dass man den angebotenen Funktionsumfang des Skripts nicht über Gebühr ausreizen sollte. Ich rate davon ab, mehr als drei verschiedene Spielfilme gleichzeitig in Angriff zu nehmen. Das könnte beim unaufmerksamen Zuschauer sonst schnell zu lustigen Verwirrungen führen, besonders wenn zum Beispiel Matt Daemon und Leonardo DiCaprio in ähnlichen Kinowerken zu Gange sind. (jcb)

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2010/05/Perl ]

[2] Tagesschau-Download in verschiedenen Formaten: [http://www.tagesschau.de/export/video-podcast/webl/tagesschau]

[03] Tivo, der digitale Videorekorder: [http://tivo.com]

[04] Michael Schilli, “Verkehrte Welt” [https://www.linux-magazin.de/Heft-Abo/Ausgaben/2010/03/tleW-etrhekreV]

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” (engl­isch) 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