Caipithek

Die Caipithek ist eine private Online-Videothek auf dem Caipirinha-Server. Sie basiert im Wesentlichen auf dem VLC Media Player, einem Shell-Skript und einem PHP-Skript.

Caipithek
Caipithek

Benutzung

Zur Benutzung der Caipithek sollte auf dem Client-Computer der VLC Media Player installiert sein. Mit diesem gelungenen Programm, welches für verschiedene Plattformen erhältlich ist, können nämlich sehr viele Video- und Audioformate wiedergegeben werden.

Nach dem Aufrufen der durch eine Anmeldung geschützte Startseite der Caipithek bekommt der Benutzer ein Auswahlfenster mit der zur Verfügung stehenden Videos angezeigt. Die maximale Anzahl der Videos, welche zur Wiedergabe ausgewählt werden können, ist über diesem Auswahlfenster angegeben.

Der Benutzer wählt nun einen oder mehrere Video aus und konfiguriert die unter dem Auswahlfenster aufgelisteten Wiedergabe-Optionen. Darunter fallen:

  • Video-Codec: Hier ist H.264 vorselektiert, weil bei diesem Format die beste Qualität erreicht wird.
  • Bandwidth: Hier wird die Bandbreite für den Stream eingestellt. Eine höhere Bandbreite bietet eine bessere Wiedergabe-Qualität, erhöht aber auch die Wahrscheinlichkeit von Aussetzern, wenn der Übertragungskanal beschränkt ist.
  • Audio Stream: Die meisten Videos haben mehrere Tonspuren. Wählt man hier 1, hat man meist den deutschen Ton, die Auswahl von 2 liefert meist den englischen Ton. Haben nicht alle ausgewählten Videos mehrere Tonspuren, so wird bei der Wiedergabe immer Tonspur 1 benutzt.


Nach Betätigen des Start-Knopfes erscheint eine neue Seite mit Angaben zu den ausgewählten Videos und zu den Wiedergabe-Optionen. Die Wiedergabe der Videos sollte nach wenigen Sekunden starten. Das Wiedergabefenster kann auch auf ein Vollbild aufgezogen werden.

Will man die Wiedergabe abbrechen, muss beachtet werden, dass man nicht einfach das Browserfenster schließen darf, sondern dass man wirklich auf den Exit-Knopf drückt, damit das Streaming des Videos auch im Server korrekt abgebrochen wird.

Man kommt dann nach einigen Sekunden wieder auf die Startseite der Caipithek zurück.

Installation

Die Caipithek basiert auf dem erfolgreichen Zusammenspiel dieser Komponenten:

Zunächst müssen aber folgende Pakete auf dem Server installiert werden:

  • gnome-vfs2
  • grep
  • mediainfo (ab Version 0.7.26)
  • sed
  • vlc
  • vlc-noX

Eventuell werden dadurch noch weitere Pakete automatisch installiert. Weiterhin müssen ein funktionierender Webserver, beispielsweise Apache, und eine korrekte PHP-Installation vorhanden sein.

Icons

Das PHP-Skript benutzt Buttons zur Visualisierung von Funktionen. Unter /usr/share/icons/gnome/32×32/actions habe ich einige schöne Buttons gefunden, welche nun zunächst über den Apache-Server “sichtbar” gemacht werden sollen. Dazu werden folgende Einträge in der Datei /etc/apache2/httpd.conf.local vorgenommen:

Alias /gnome-icons     /usr/share/icons/gnome/32x32

<Directory /usr/share/icons/gnome/32x32>
    Order             allow,deny
    Allow from all
</Directory>

Danach muss die geänderte Konfiguration noch mit /etc/init.d/apache2 reload oder mit /etc/init.d/apache2 restart aktiviert werden.

VLC-Serverprozeß

Dann wird der VLC-Serverprozeß mit einem telnet-Interface im Hintergrund gestartet. Ich verwende dazu den gleichen Benutzer (wwwrun) wie für den Apache-Webserver. Für diesen Benutzer muss dann aber auch eine Login-Shell existieren; man also beispielsweise für wwwrun in der Benutzer-Verwaltung /bin/bash anstatt /bin/false einstellen. Dementsprechend sollte man auch ein sicheres Passwort wählen. Die entsprechenden Befehle sehen bei mir in einer root-Konsole also so aus:

caipirinha:~ # su -l wwwrun
wwwrun@caipirinha:~> cvlc -I oldtelnet --telnet-password geheimes_passwort > cvlc.log 2>&1 &
[1] 21639
wwwrun@caipirinha:~> exit
logout
caipirinha:~ #

geheimes_passwort muss natürlich durch ein selbstgewähltes Passwort ersetzt werden. Wichtig ist die Option oldtelnet, welche seit VLC 1.1.0 benutzt werden muss (davor war es nur telnet). Damit wurde nun VLC im Hintergrund gestartet, und auf Port 4212 kann man sich nun mit dem VLC-Server über telnet verbinden. Der VLC-Serverprozeß ist der Kern der Videothek. Über diesen Prozeß wird der Video-Stream erzeugt und bei Bedarf auch transkodiert (also große Videos auf eine kleine Bitrate “herunter gerechnet”).

Shell-Skript

Das nächste wichtige Element ist ein Shell-Skript (filmliste.sh), welches über cron regelmäßig aufgerufen wird, in meinem Fall immer montags. Dieses Shell-Skript erstellt eine Liste aller Videos mit einigen Zusatzinformationen. Dazu durchsucht es mit dem Befehl gnomevfs-info das Verzeichnis VIDEODIR nach Videos, extrahiert mit dem Programm mediainfo, und zwar ab Version 0.7.26. Einzelheiten über as Format, die Anzahl der Video-Streams, die Anzahl der Audio-Streams und die Länge des Videos und schreibt dann für jede gefundene Video eine Zeile in die Datei VIDEOLIST.

filmliste.sh:

#!/bin/bash
#
# This script searches for video files in the specified folder and creates an ordered list of the files.
#
# Gabriel Rüeck, gabriel@caipirinha.homelinux.org, 18.01.2010
#

# Pre-define some variables and read the configuration files.
readonly VIDEODIR='/home/public/Video'
readonly VIDEOLIST='/home/public/Video/Filmliste.txt'

# Delete the old file
rm "$VIDEOLIST"

# Scan the specified directory for a sorted list of files that can be read by "others".
cd "$VIDEODIR" && find . -perm -004 -type f -print 2>/dev/null | sort |
                       # The following commands will be executed in a subshell; they would anyway because they follow the pipe (|) symbol.
                       # But first the variable IFS is set to "|" as separator. Normally, IFS is set to " ". This is important for the read command as IFS
                       # contains the character that is used as a separator for the read command. A " " as separator leads to problems when the file name has
                       # a double white space like "  " or so in the name as read would report this back as a single white space " ". That !$#% cost me a day...
                       (OLD_IFS=$IFS
                        IFS=""
                        while read -r FILE_NAME;
                        # Use gnomevfs-info as it determines videos with a higher reliability than the file command. Test if the MIME type starts with "video/".
                        # If that is the case, store the MIME type in $VIDEO_TYPE, otherwise set $VIDEO_TYPE to "".
                        do VIDEO_TYPE=$(gnomevfs-info $FILE_NAME | sed -n 's/\(^MIME type *:\) \(.*\)/\2/p' | fgrep 'video/')
                           if [ $VIDEO_TYPE ]; then
                              # If a video stream has been identified, store the result in the output file. Use ":" as separator.
                              DATA=$(mediainfo -f "$FILE_NAME")
                              WIDTH=$(echo $DATA | sed -n 's/^Width[^:]*:[^0-9]*\([0-9].*\)/\1/p' | head -n1)
                              HEIGHT=$(echo $DATA | sed -n 's/^Height[^:]*:[^0-9]*\([0-9].*\)/\1/p' | head -n1)
                              DURATION=$[$(echo $DATA | sed -n 's/^Duration[^:]*:[^0-9]*\([0-9].*\)/\1/p' | head -n1)/1000]
                              VIDEO_STREAMS=$(echo $DATA | sed -n 's/^Count of video streams[^:]*:[^0-9]*\([0-9].*\)/\1/p' | head -n1)
                              AUDIO_STREAMS=$(echo $DATA | sed -n 's/^Count of audio streams[^:]*:[^0-9]*\([0-9].*\)/\1/p' | head -n1)
                              echo -e "${FILE_NAME:2}:$WIDTH:$HEIGHT:$VIDEO_STREAMS:$AUDIO_STREAMS:$DURATION" >> $VIDEOLIST
                           fi
                        done;
                        # Reset IFS to its old value
                        IFS=$OLD_IFS)

Das Skript läuft unter einem normalen Benutzer-Account und sucht sowieso nur nach Dateien, die für others lesbar sind. Die resultierende Text-Datei hat dann beispielsweise Einträge wie diese:

_BRASILIEN/Cidade dos Homens/Folge 1.avi:672:512:1:2:2121
_BRASILIEN/Cidade dos Homens/Folge 2.avi:672:496:1:2:1646
_BRASILIEN/Cidade dos Homens/Folge 3.avi:672:464:1:2:1781
_BRASILIEN/Cidade dos Homens/Folge 4.avi:672:496:1:2:1895

Alle Einträge einer Zeile sind durch einen Doppelpunkt getrennt. Für jedes Video werden folgende Informationen erfasst:

  • Pfad und Name des Videos, relativ zu VIDEODIR
  • Breite des Videos in Pixeln
  • Höhe des Videos in Pixeln
  • Anzahl der Video-Streams
  • Anzahl der Audio-Streams
  • Länge des Videos in Sekunden

Damit dieses Skript erfolgreich funktioniert, dürfen allerdings weder im Pfadnamen noch im Dateinamen Doppelpunkte vorkommen.

Konfigurationsdatei

Das PHP-Skript der Caipithek selbst benötigt noch eine Konfigurationsdatei namens .caipithek, welche sich im gleichen Verzeichnis wie caipithek.php befinden muss. Hier ist ein Beispiel einer funktionierenden Konfigurationsdatei:

.caipithek

;;;;;;;;;;;;;;
; File Paths ;
;;;;;;;;;;;;;;

movie_list = /home/public/Video/Filmliste.txt
movie_path = /home/public/Video/
log_file   = /var/lib/wwwrun/caipithek.log

;;;;;;;;;;;;;;;;;;;;
; System Variables ;
;;;;;;;;;;;;;;;;;;;;

max_streams = 1
max_inputs  = 2

max_width  = 1024
max_height = 576

vlc_host   = localhost
vlc_port   = 4212
vlc_pwd    = geheimes_passwort
vlc_stream = 192.168.2.2:8008

public_add = caipirinha.homelinux.org:8008

Die meisten Einträge in der Konfigurationsdatei sind selbsterklärend. geheimes_passwort ist das gleiche selbstgewählte Passwort, welches schon beim Starten des VLC-Serverprozesses angegeben worden ist. max_streams legt die Anzahl der gleichzeitig transkodierten Streams fest. Auf dem Caipirinha-Server wären hier alleine von der Rechenleistung her maximal 2 gleichzeitige Streams möglich, aber bei der geringen Upload-Rate meines ADSL-Anschlusses habe ich max_streams auf 1 gesetzt. max_inputs legt die maximale Anzahl der Videos fest, welche zur direkt aufeinander folgenden Wiedergabe ausgewählt werden können. max_width und max_height definieren die maximale Größe, die beim Streamen über das Internet möglich ist. Damit werden Videos in HD-Auflösung herunter skaliert, weil die volle HD-Auflösung beim geringen Upload der ADSL-Verbindung zu ruckelnden Bildern führen würde. Beim reinen Streamen eines Videos (ohne Transkodierung), was nur im LAN möglich ist, können auch Videos in HD-Auflösung wiedergegeben werden. vlc_stream legt die Adresse und den Port fest, an dem der VLC-Serverprozeß den Stream anbieten wird. Die IP-Adresse muss natürlich die IP-Adresse der Maschine sein, auf der der VLC-Serverprozeß selbst läuft. Mit public_add definiert man die IP-Adresse und den Port, wie man auf den Stream vom Internet aus zugreift. Deshalb ist hier auch der FQDN (caipirinha.homelinux.org) angegeben. Der Port ist in meiner Konfiguration der gleiche wie in der Variable vlc_stream, weil ich den entsprechenden Port auf dem ADSL-Router direkt zum Caipirinha-Server durchreiche. log_file legt schließlich fest, wohin die Log-Datei geschrieben werden soll. Fehlt dieser Eintrag, so werden vom PHP-Skript dennoch Log-Einträge erzeugt, aber nach /dev/null geschrieben.

PHP-Skript

Das PHP-Skript stellt die Schnittstelle der Caipithek zum Benutzer dar. Es liest die vom Shell-Skript erzeugte Filmliste und die Konfigurationsdatei ein. Das Skript arbeitet mit Session Cookies und kann 3 verschiedene Stati annehmen:

  • Status 1: Die Einstiegsseite wird angezeigt, Videos werden in einem Auswahlfenster angezeigt und Optionen zum Transkodieren oder (im LAN) zum reinen Streaming werden angeboten.
  • Status 2: Die ausgewählten Videos werden transkodiert und gestreamt (oder im LAN nur gestreamt) und im Browser-Fenster des Client-Computers mittels eines Plugin wiedergegeben.
  • Status 3: Der Wiedergabekanal wird entfernt, und das PHP-Skript geht automatisch in Status 1 über.

Filmauswahl

Das PHP-Skript liest die vom Shell-Skript erzeugte Filmliste ein und bereitet die darin enthaltenen Informationen visuell auf. Darüber hinaus werden Wiedergabe-Optionen zur Auswahl angeboten:

  • Video-Codec: Hier ist H.264 vorselektiert, weil bei diesem Format die beste Qualität erreicht wird.
  • Bandwidth: Hier wird die Bandbreite für den Stream eingestellt. Da mein ADSL-Anschluß nur etwa 630 kbps im Uplink erlaubt, wird hier normalerweise 544 kbps als maximale Bandbreite angeboten (damit mein Uplink nicht ganz “dicht” gemacht wird).
  • Audio Stream: Die meisten der Videos haben eine deutsche und eine englische Tonspur. Wählt man hier 1, hat man also meist den deutschen Ton, die Auswahl von 2 liefert dann den englischen Ton. Die hier gesetzte Auswahl wird aber nur dann berücksichtigt, wenn alle ausgewählten Videos auch mehrere Tonspuren haben. Ansonsten wird nur die Tonspur 1 wiedergegeben.

Nach Betätigen des Start-Knopfen wechselt das Skript in den Status 2 zur Wiedergabe der ausgewählten Videos.

Wiedergabe

In Abhängigkeit der gewählten Videos und Optionen werden jetzt Werte festgelegt für:

  • den MIME-Typ des zu übertragenden Streams ($mimetype)
  • den Transport-Multiplexer ($mux)
  • die Video-Bitrate ($vb)
  • die Audio-Bitrate ($ab)
  • die maximale Auflösung, welche sich aus der maximalen Breite aller Videos und der maximalen Höhe aller Videos zusammen setzt und ggf. bei HD-Videos noch weiter beschränkt wird ($width und $height)
  • die Audio-Spur, welche übertragen wird ($audio)

Dabei werden bereits die gewählten Filme über einen telnet-Kanal auf Port 4212 zum im Hintergrund laufenden VLC-Serverprozeß übertragen und auch in der Log-Datei vermerkt. Außerdem wird eine zufällig generierte Kanalnummer erzeugt. Diese hat den Zweck, den Zugriff auf den Video-Stream für Unbefugte möglichst zu erschweren. Sicherer wäre es natürlich, hier noch einmal eine Authentifizierung des Benutzers vorzunehmen, und VLC sieht ja auch eine entsprechende Möglichkeit vor [1]. Allerdings wirkt sich ein zusätzliches Popup-Fenster zur Eingabe eines Banutzernamens und eines Passwortes nachteilig auf das Handling der Webseite aus. Eine weitere Überlegung wäre auch gewesen, den Datenstrom zu verschlüsseln [2]. Es gibt also durchaus Spielraum, den Streaming-Vorgang noch weiter abzusichern. Hier wurde nur der zugegebenermaßen “unsichere” Weg der zufällig erzeugten Kanalnummer gewählt.

In Abhängigkeit vom gewählten Container-Format (ts oder asf) wird nun mit Active-X entweder ein VLC-Plugin oder ein Media Player-Plugin aktiviert. Den HTML-Code dazu habe ich aber selbst auch von verschiedenen Web-Seiten [3] [4] [5] [6] übernommen; er stammt nicht von mir selbst.

Bei der Caipithek habe ich mich dafür entschieden, zwei Player zu unterstützen, den VLC Media Player und den Windows Media Player. Der Windows Media Player kann allerdings meines Wissens Streams nur im ASF-Format wiedergeben und kommt außerdem in der Standard-Installations mit nur wenigen Video-Codecs daher. Deswegen und wegen der in [7] aufgelisteten Beschränkungen habe ich mich entschieden, folgende Zuordnung vorzunehmen:

  • asf-Container haben WMV2 als Video-Format
  • ts-Container haben H.264-, MPEG4- oder MPEG2 als Video-Format

Für Hinweise zu anderen Lösungen bin ich aber jederzeit sehr dankbar.

Wiedergabe beenden

Ein kleiner Schönheitsfehler ist, dass der Benutzer nicht einfach den Browser schließen darf, wenn er das Streaming abbrechen will, sondern dass er auf den Exit-Knopf drücken muss. Erst dann wird nämlich auch das Transkodieren und Streamen im Server unterbrochen. Schließt der Benutzer einfach das Browser-Fenster, ohne vorher auf Exit zu drücken, läuft die Transkodierung eventuell weiter und blockiert einen der durch max_streams erlaubten Kanäle, bis alle Videos abgelaufen sind. Auf jeden Fall wird beim Aufruf der Startseite der Caipithek nach Kanälen mit bereits abgelaufenen Filmen gesucht, die dann gelöscht werden. Dieser Vorgang wird dann auch in den Log-Dateien vermerkt.

caipithek.php
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<html>
<head>
  <title>Caipithek</title>
  <style type="text/css">
    a:link { text-decoration:underline; font-weight:normal; color:#0000FF; }
    a:visited { text-decoration:underline; font-weight:normal; color:#800080; }
    a:hover { text-decoration:underline; font-weight:normal; color:#909090; }
    a:active { text-decoration:blink; font-weight:normal; color:#008080; }
    h1 { font-family:Arial,Helvetica,sans-serif; font-size:small; color:maroon; text-indent:0.0cm; }
    h2 { font-family:Arial,Helvetica,sans-serif; font-size:small; color:green; text-indent:0.0cm; }
    h3 { font-family:Arial,Helvetica,sans-serif; font-size:small; color:black; text-indent:0.0cm; }
    h4 { font-family:Arial,Helvetica,sans-serif; font-size:x-small; color:black; text-indent:0.5cm; }
    h5 { font-family:Arial,Helvetica,sans-serif; font-size:x-small; color:black; text-indent:1.0cm; }
    hr { text-indent:0.0cm; height:3px; width:100%; text-align:left; }
    li { font-family:Courier New; font-size:x-small; color:blue; }
    p { font-family:Arial,Helvetica,sans-serif; font-size:x-small; color: black; text-indent:0.0cm; }
    th { font-family:Arial,Helvetica,sans-serif; font-size:x-small; color: black; text-indent:0.0cm; }
    td { font-family:Courier New,sans-serif; font-size:x-small; color: black; text-indent:0.0cm; }
    body { background-color:#FFFFD8; padding:0px; }
    div.mybody { margin-left:100px; margin-top:20px; margin-right:20px; margin-bottom:20px; }
  </style>
  <link rel="shortcut icon" href="http://caipirinha.homelinux.org/favicon.ico">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta http-equiv="content-language" content="de">
  <meta name="author" content="root">
  <meta name="date" content="2010-08-12T17:30:00+0200">
  <meta name="description" lang="de" content="Caipirinha Videothek">
  <meta name="robots" content="noindex">
</head>

<body>
<?php
  /* FUNCTIONS */
  function read_until_prompt ($fp) {
     /* Read until a '>' char is found. */
     do;
     while (fgetc($fp) != ">");
     /* Read the following space char. */
     fgetc($fp);
  }

  function chk_stream ($fp) {
     /* Read until a trimmed (bare of leading and trailing spaces) line starting with the word 'output' is found. */
     do $line = trim(fgets($fp,200));
     while (substr($line,0,6) != "output");
     return (substr($line,9,9) == "#standard");
  }

  function read_until_instances ($fp) {
     /* Read until a trimmed (bare of leading and trailing spaces) line with the word 'instances' is found. */
     do;
     while (trim(fgets($fp,200)) != "instances");
  }

  /* PROGRAM CODE */
  /* Read the configuration file. */
  $cfg_db = dba_open (".caipithek","r","inifile") or die ("Caipithek config file could not be found.");
  $movie_list = dba_fetch ("movie_list",$cfg_db) or die ("The variable 'movie_list' has not been specified.");
  $movie_path = dba_fetch ("movie_path",$cfg_db) or die ("The variable 'movie_path' has not been specified.");
  $log_file = dba_fetch ("log_file",$cfg_db) or $log_file="/dev/null";
  $max_streams = dba_fetch ("max_streams",$cfg_db) or $max_streams=2;
  $max_inputs = dba_fetch ("max_inputs",$cfg_db) or $max_inputs=2;
  $max_width =  dba_fetch ("max_width",$cfg_db) or $max_width=960;
  $max_height =  dba_fetch ("max_height",$cfg_db) or $max_height=540;
  $vlc_host = dba_fetch ("vlc_host",$cfg_db) or $vlc_host="localhost";
  $vlc_port = dba_fetch ("vlc_port",$cfg_db) or $vlc_port=4212;
  $vlc_pwd  = dba_fetch ("vlc_pwd",$cfg_db) or $vlc_pwd="admin";
  $vlc_stream = dba_fetch ("vlc_stream",$cfg_db) or die ("The variable 'vlc_stream' has not been specified.");
  $public_add = dba_fetch ("public_add",$cfg_db) or die ("The variable 'public_add' has not been specified.");
  dba_close ($cfg_db);

  /* This program has different states (like a state machine:
     1: This is the DEFAULT state. In this mode, the list of movies and the coding options are displayed.
     2: This is the PLAY state. In this mode, the movies are being transcoded and displayed on the client browser.
        The user can change to state 3 or state 4 from here.
     3: This is the STOP state. The channel is deleted from the VLC server.
        State 3 is automatically left for state 1 after some seconds.                                               */

  /* Determine what the page shall display. */
  if (!count($_POST))
     $state = 1;
  else
     if (isset($_POST['state'])) {
        $state = $_POST['state'];
        if ($state == 2)
           if (isset($_POST['movies']) and isset($_POST['vcodec']) and isset($_POST['audio'])) {
              $movies = $_POST['movies'];
              $vcodec = $_POST['vcodec'];
              $audio  = (int) $_POST['audio']; /* Without the explicit (int) the variable would be read as a string. */
              isset($_POST['bw']) ? $bw=$_POST['bw'] : $bw=1; /* $bw may be unset for local access when 'streaming' is pre-selected. */
           } else $state = 1;
        if ($state == 3)
           if (isset($_POST['channel']))
              $channel = $_POST['channel'];
           else $state = 1;
      } else
         $state = 1;

  /* Determine if the client is on the local network. If yes, set $is_local=true. */
  $is_local = false;
  unset($output);
  exec('/sbin/ifconfig | sed -n \'s/.*inet [^0-9]*\([0-9]\{1,\}.[0-9]\{1,\}.[0-9]\{1,\}.[0-9]\{1,\}\).*M[^0-9]*\([0-9]\{1,\}.[0-9]\{1,\}.[0-9]\{1,\}.[0-9]\{1,\}\).*/\1.\2/p\'',$output);
  foreach ($output as $ip_pair) {
           /* Determine the local network. */
           $local_ip = (int) strtok($ip_pair,".");
           for ($counter=1; $counter<4; $counter++)
                $local_ip = $local_ip<<8 | (int) strtok(".");
           $netmask = (int) strtok(".");
           for ($counter=1; $counter<4; $counter++)
                $netmask = $netmask<<8 | (int) strtok(".");
           /* Determine the remote network. */
           $remote_ip = (int) strtok($_SERVER['REMOTE_ADDR'],".");
           for ($counter=1; $counter<4; $counter++)
                $remote_ip = $remote_ip<<8 | (int) strtok(".");
           /* Check if $local_ip and $remote_ip belong to the same subnet. */
           $is_local |= ($local_ip & $netmask) == ($remote_ip & $netmask);
  }

  /* Open log file. */
  $log_ptr = fopen ($log_file,"a") or die ("The file $log_file cannot be opened.");

  /* Open telnet session to VLC server process and log in with $vlc_pwd. */
  if (!($vlc_ptr = fsockopen($vlc_host,$vlc_port,$errno,$errdesc,4))) {
      print ("<p style=\"color:red\">An error has occured. The VLC Server cannot be contacted: ".$errno." - ".$errdesc."</p>\n");
      print ("<p>The error has been logged.</p>\n");
      print ("</body>\n</html>\n");
      fprintf($log_ptr,"%s: The VLC Server is unavailable: %s, %s\n",date("Y-m-d H:i:s"),$errno,$errdesc);
      fclose ($log_ptr);
      exit (1);
  }
   fgets($vlc_ptr,14);
  fputs($vlc_ptr,$vlc_pwd."\n");
  read_until_prompt($vlc_ptr);

  switch ($state) {
       case 2:
        print ("<h1>Caipithek - Watch Movie</h1>\n");
        $width = 9999;
        $height = 9999;
        switch ($vcodec) {
           case "h264": $mimetype="video/3gpp";
                        $mux="ts";
                        break;
           case "mp4v": $mimetype="video/mp4";
                        $mux="ts";
                        break;
           case "mp2v": $mimetype="video/mpeg";
                        $mux="ts";
                        break;
           case "WMV2": $mimetype="video/x-ms-wmv";
                        $mux="asf";
                        break;
           default:     $mimetype="video/mp4";
                        $mux="ts";                    /* Then, VLC rather than Windows Media Player will be selected for pure streaming.*/
        }
        switch ($bw) {
           case 2:  $vb =   320; /* 384 kbps in total */
                    $ab =    64;
                    break;
           case 3:  $vb =   384; /* 448 kbps in total */
                    $ab =    64;
                    break;
           case 4:  $vb =   448; /* 544 kbps in total */
                    $ab =    96;
                    break;
           case 5:  $vb =  1024; /* 1152 kbps in total; only offered when acces happens from local network */
                    $ab =   128;
                    break;
           default: $vb =  208; /* 256 kbps in total */
                    $ab =   48;
        }
        $channel = "V".rand(100000000,999999999);
        fputs($vlc_ptr,"new ".$channel." broadcast enabled\n");
        read_until_prompt($vlc_ptr);
        print("<p>The following movies will now be streamed:</p>\n<ul>\n");
        foreach ($movies as $movie) {
           if (!$max_inputs--) break;
           /* Separate the movie name, the movie width and the movie height from the input. */
           $movie_name = base64_decode(strtok($movie,":"));
           /* Determine the smallest image size of all selected movies as this determines the resolution of the stream. */
           $width = min(strtok(":"),$width);
           $height = min(strtok(":"),$height);
           $audio = min(strtok(":"),$audio);
           fputs($vlc_ptr,"setup ".$channel." input \"".$movie_path.$movie_name."\"\n");
           read_until_prompt($vlc_ptr);
           print("<li>".$movie_name."</li>\n");
           /* Store the movie names in the log file. */
           fprintf($log_ptr,"%s, %s: Channel %s linked to movie '%s'.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$channel,$movie_name);
        }
        print("</ul>\n");
        if (($vcodec == "stream") or (($width <= $max_width) and ($height <= $max_height))) {
           print("<p>The smallest image size of all the selected movies determines the resolution of the video stream. It has been set to: <b>".$width."</b> x <b>".$height."</b> pixels.");
        } else {
           /* Limit width and height of the image for transcoded streams so that the movie remains fluent. */
           $width = min ($width,$max_width);
           $height = min ($height,$max_height);
           print("<p>The resolution of the video stream has been reduced in order to cater for the limited bandwidth. It has been set to: <b>".$width."</b> x <b>".$height."</b> pixels.");
        }
        print(" Audio channel <b>".$audio."</b> has been selected.</p>\n");
        /* Select Audio Channel. Usually, my movies are encoded with two audio streams. */
        fputs($vlc_ptr,"setup ".$channel." option audio-track=".($audio-1)."\n");
        read_until_prompt($vlc_ptr);
        print("<p>Streaming will be done using the channel: <a href=\"http://".$public_add."/".$channel."\"><b>http://".$public_add."/".$channel."</b></a></p><hr>\n");
        if ($vcodec == "stream") {
           /* If pure streaming has been selected, do not transcode, but just stream the movie. */
           fputs($vlc_ptr,"setup ".$channel." output #standard{access=http{mime=\"".$mimetype."\"},mux=ts,dst=".$vlc_stream."/".$channel."}\n");
           /* Update the channel status in the log file. */
           fprintf($log_ptr,"%s, %s: Channel %s streaming without transcoding. Audio stream %d is selected.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$channel,$audio);
        } else {
            /* If a bandwidth has been selected, transcode the movie. */
            fputs($vlc_ptr,"setup ".$channel." output #transcode{vcodec=".$vcodec.",vb=".$vb.",width=".$width.",height=".$height.",acodec=mp3,ab=".$ab.",channels=2}:standard{access=http{mime=\"".$mimetype."\"},mux=".$mux.",dst=".$vlc_stream."/".$channel."}\n");
           /* Update the channel status in the log file. */
           fprintf($log_ptr,"%s, %s: Channel %s streaming with transcoding to %s. Audio stream %d is selected.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$channel,$vcodec,$audio);
        }
        read_until_prompt($vlc_ptr);
        fputs($vlc_ptr,"control ".$channel." play\n");
        read_until_prompt($vlc_ptr);

        /* Das folgende Konstrukt spricht sowohl den Internet Explorer als auch Firefox an.
            Für den Internet-Explorer wird das <object>-Tag ausgeführt, aber der Inhalt des <comment>-Tags überlesen.
            Für Firefox wird im <object>-Tag alles bis auf den Inhalt des <comment>-Tags überlesen. Letzteres wird aber dann ausgeführt. */
        if ($mux == "ts") {
           print("<object classid=\"clsid:E23FE9C6-778E-49D4-B537-38FCDE4887D8\" codebase=\"http://downloads.videolan.org/pub/videolan/vlc/latest/win32/axvlc.cab\" mimetype=\"".$mimetype."\" width=\"".$width."\" height=\"".$height."\" id=\"vlc\" events=\"True\">\n");
           print("<param name=\"Src\" value=\"http://".$public_add."/".$channel."\" />\n");
           print("<param name=\"ShowDisplay\" value=\"True\" />\n");
           print("<param name=\"AutoPlay\" value=\"True\" />\n");
           print("<param name=\"Volume\" value=\"100\" />\n");
           print("<embed src=\"http://".$public_add."/".$channel."\" mimetype=\"".$mimetype."\" border=\"2\" width=\"".$width."\" height=\"".$height."\" id=\"vlc\"></embed>\n</object><hr>\n");
           /* User Interface with icons. */
           print("<table width=\"100%\" cellspacing=\"1\" cellpadding=\"4\" bgcolor=\"beige\"><tr>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"image\" src=\"/gnome-icons/actions/media-playback-start.png\" class=\"submit\" alt=\"Play\" onClick=\"document.vlc.play();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"image\" src=\"/gnome-icons/actions/media-playback-pause.png\" class=\"submit\" alt=\"Pause\" onClick=\"document.vlc.pause();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"image\" src=\"/gnome-icons/status/audio-volume-muted.png\" class=\"submit\" alt=\"Mute\" onClick=\"document.vlc.toggleMute();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"image\" src=\"/gnome-icons/actions/view-fullscreen.png\" class=\"submit\" alt=\"Full Screen\" onClick=\"document.vlc.fullscreen();\"></td>\n");
           print("<td width=\"56%\" align=\"center\" valign=\"center\"><form action=\"".$_SERVER['PHP_SELF']."\" method=\"post\">\n");
           print("<input type=\"hidden\" name=\"state\" value=\"3\">\n");
           print("<input type=\"hidden\" name=\"channel\" value=\"".$channel."\">\n");
           print("<input type=\"image\" src=\"/gnome-icons/actions/media-playback-stop.png\" class=\"submit\" alt=\"Stop\">");
           print("</form></td>\n</tr></table>\n");
           /* User Interface with text buttons. */
           print("<table width=\"100%\" cellspacing=\"1\" cellpadding=\"2\"><tr>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"button\" class=\"submit\" style=\"background-color:aquamarine;font-weight:bold\" value=\"Play\" onClick=\"document.vlc.play();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"button\" class=\"submit\" style=\"background-color:aquamarine;font-weight:bold\" value=\"Pause\" onClick=\"document.vlc.pause();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"button\" class=\"submit\" style=\"background-color:aquamarine;font-weight:bold\" value=\"Mute\" onClick=\"document.vlc.togglemute();\"></td>\n");
           print("<td width=\"11%\" align=\"center\" valign=\"center\"><input type=\"button\" class=\"submit\" style=\"background-color:aquamarine;font-weight:bold\" value=\"Full Screen\" onClick=\"document.vlc.fullscreen();\"></td>\n");
           print("<td width=\"56%\" align=\"center\" valign=\"center\"><form action=\"".$_SERVER['PHP_SELF']."\" method=\"post\">\n");
           print("<input type=\"hidden\" name=\"state\" value=\"3\">\n");
           print("<input type=\"hidden\" name=\"channel\" value=\"".$channel."\">\n");
           print("<input type=\"submit\" value=\"Exit\" style=\"background-color:lightcoral;font-weight:bold\">");
           print("</form></td>\n</tr></table>\n");
        } else {
           print("<object id=\"mediaPlayer\" classid=\"CLSID:22d6f312-b0f6-11d0-94ab-0080c74c7e95\" codebase=\"http://activex.microsoft.com/activex/controls/mplayer/en/nsmp2inf.cab#Version=5,1,52,701\" standby=\"Loading Microsoft Windows Media Player components...\" type=\"application/x-oleobject\">\n");
           print("<param name=\"fileName\" value=\"http://".$public_add."/".$channel."\">\n");
           print("<param name=\"animationatStart\" value=\"true\">\n");
           print("<param name=\"transparentatStart\" value=\"true\">\n");
           print("<param name=\"autoStart\" value=\"true\">\n");
           print("<param name=\"showControls\" value=\"true\">\n");
           print("<param name=\"Volume\" value=\"-100\">\n");
           print("<embed src=\"http://".$public_add."/".$channel."\" mimetype=\"".$mimetype."\" border=\"2\" width=\"".$width."\" height=\"".$height."\" id=\"mediaPlayer\"></embed>\n</object><hr>\n");
           /* User Interface with 'Exit' buttons. */
           print("<form action=\"".$_SERVER['PHP_SELF']."\" method=\"post\">\n");
           print("<input type=\"hidden\" name=\"state\" value=\"3\">\n");
           print("<input type=\"hidden\" name=\"channel\" value=\"".$channel."\">\n");
           print("<table cellspacing=\"1\" cellpadding=\"4\" bgcolor=\"beige\"><tr>\n");
           print("<td><input type=\"image\" src=\"/gnome-icons/actions/media-playback-stop.png\" class=\"submit\" alt=\"Stop\"></td>\n");
           print("<td><input type=\"submit\" value=\"Exit\" style=\"background-color:lightcoral;font-weight:bold\"></td>\n</tr></table>\n");
           print("</form>\n");
        }
     break;

     case 3:
        /* Stop the transmission and delete the channel. After this, run through to the default stage. */
        fputs($vlc_ptr,"control ".$channel." stop\n");
        read_until_prompt($vlc_ptr);
        fputs($vlc_ptr,"del ".$channel."\n");
        read_until_prompt($vlc_ptr);

        /* Store the deleted channel in the log file. */
        fprintf($log_ptr,"%s, %s: Channel %s destroyed.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$channel);

     default:
        print ("<h1>Caipithek - Movie Selector</h1>\n");

        /* Check the number of active channels. */
        fputs($vlc_ptr,"show media\n");
        fgets($vlc_ptr,10);
        /* Get the number of broadcast channels which have already been set up. */
        preg_match_all ("/\b([0-9]{1,}) broadcast\b/",fgets($vlc_ptr,38),$output,PREG_SET_ORDER);
        $broadcasts = $output[0][1];
        /* If channels have been defined, examine them and remove the "dead" ones. */
        if ($broadcasts) {
           /* Read existing channel. */
           $channel = trim(fgets($vlc_ptr,20));
           $loop = true;
           unset ($dead_channels);
           do {
              /* Check if the current channel is transcoded or only streamed. */
              $is_streamed = chk_stream($vlc_ptr);
              read_until_instances($vlc_ptr);
              /* Continue to read until a '>', a digit ([0-9]) or a word starting with 'i' (for 'instances') is encountered. */
              do $char = fgetc($vlc_ptr);
              while (strpos(">Vi",$char) === false);
              if ($char == '>') {
                 /* This channel is not active. Put the channel number into the list of dead channels. */
                 $dead_channels[] = $channel;
                 /* Decrease the number of broadcasts. */
                 $broadcasts--;
                 /* Read the following space char. */
                 fgetc($vlc_ptr);
                 $loop = false;
              } elseif ($char == 'i') {
                 /* This channel is active. Overread all information until a line with the word 'playlistindex' is read. */
                 do;
                 while (substr(trim(fgets($vlc_ptr,40)),0,13) != "playlistindex");
                 /* Despite the channel being active - check whether the stream is transcoded or streamed only.
                    If the channel is streamed only, decrease $broadcasts as streaming uses little CPU power only. */
                 if ($is_streamed)
                     $broadcasts--;
                 /* Read next character. */
                 $char = fgetc($vlc_ptr);
                 if ($char == '>') {
                    /* Read the following space char and abort the loop. */
                    fgetc($vlc_ptr);
                    $loop = false;
                 } else {
                    /* Another channel declaration is following. Concatenate the read digit with the rest of the digits. */
                    $channel = trim(fgets($vlc_ptr,20));
                 }
              } else {
                 /* The current channel is not active. Put the channel number into the list of dead channels. */
                 $dead_channels[] = $channel;
                 /* Decrease the number of broadcasts. */
                 $broadcasts--;
                 /* Another channel declaration is following. Concatenate the "V" with the rest of the digits. */
                 $channel = "V".trim(fgets($vlc_ptr,20));
              }
           } while ($loop);
           if (isset($dead_channels))
              /* If dead channels exist, delete them all. */
              foreach ($dead_channels as $channel) {
                       /* Delete the dead channel and write a log entry. */
                       fputs($vlc_ptr,"del ".$channel."\n");
                       read_until_prompt($vlc_ptr);
                       fprintf($log_ptr,"%s, %s: Channel %s purged.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$channel);
              }
        }

        if (($broadcasts >= $max_streams) and !$is_local) {
           /* If the client is not local and the maximum number of transcoded streams has been exceded, refuse any further streaming. */
           print ("<p>I am sorry but the maximum number of transcoded streams (<b>".$max_streams."</b>) for this server has already been reached.</p>\n");
           print ("<p>Please try this page again later.</p>\n");
           print ("<form action=\"".$_SERVER['PHP_SELF']."\" method=\"post\">\n");
           print ("<input type=\"hidden\" name=\"state\" value=\"1\">\n");
           print ("<input type=\"submit\" value=\"Retry\" style=\"background-color:springgreen;font-weight:bold\">\n");
           print ("</form>");
           /* Store the service refusal in the log file. */
           fprintf($log_ptr,"%s, %s: Maximum number of streams (%d) reached.\n",date("Y-m-d H:i:s"),$_SERVER['REMOTE_ADDR'],$max_streams);
        } else {
           /* Read the list of movies and display the selector. */
           $mov_ptr = fopen ($movie_list,"r") or die ("The file $movie_list could not been found.");
           print ("<p>Select up to <b>".$max_inputs."</b> movies from the list for streaming:</p>\n");
           print ("<form action=\"".$_SERVER['PHP_SELF']."\" method=\"post\">\n");
           print ("<select name=\"movies[]\" multiple=\"multiple\" size=\"20\">\n");
           while (!feof($mov_ptr)) {
                 $movie_name = strtok(fgets($mov_ptr),":");
                 if ($movie_name) {
                    $width = strtok(":");
                    $height = strtok(":");
                    $video_streams = strtok(":");
                    $audio_streams = strtok(":");
                    $duration = strtok(":");
                    $hours = (int) ($duration / 3600);
                    $mins = (int) (($duration % 3600) / 60);
                    $secs = $duration % 60;
                    /* The value that is transmitted for each selected movie, comprises the movie name, the image width and the image height, separated by ':'. */
                    printf ("<option value=\"%s:%d:%d:%d\">%s   [%d x %d]  [%d-%d]  (Dauer: %d:%02d:%02d)</option>\n",base64_encode($movie_name),$width,$height,$audio_streams,$movie_name,$width,$height,$video_streams,$audio_streams,$hours,$mins,$secs);
                 }
           }
           print ("</select>\n");
           print ("<p>Adjust the streaming options and click on the <b>Stream</b> button to proceed or on the <b>Clear</b> button to clear your selection.</p>\n");
           if ($is_local)
              print ("<p>Access from a <b>local network</b> has been detected. Additional bandwidth options and pure streaming are available therefore.</p>\n");

           print ("<table border bgcolor=\"lavender\" cellspacing=\"3\" cellpadding=\"5\">\n<tr><th bgcolor=\"tan\">Video Codec</th><th bgcolor=\"#E7C69A\">Bandwidth</th><th bgcolor=\"#FCD8A8\">Audio Stream</th></tr>\n");

           if ($broadcasts < $max_streams) {
              /* If the number of transcoded streams has not yet been exceeded, show the options for the video codec and for the bandwidth. */
              if ($is_local)
                 print ("<tr><td bgcolor=\"tan\"><input type=\"radio\" name=\"vcodec\" value=\"h264\"> H.264<br>\n");
              else
                 /* Pre-select H.264 for remote clients. */
                 print ("<tr><td bgcolor=\"tan\"><input type=\"radio\" name=\"vcodec\" value=\"h264\" checked> H.264<br>\n");
              print ("<input type=\"radio\" name=\"vcodec\" value=\"mp4v\"> MPEG 4<br>\n");
              print ("<input type=\"radio\" name=\"vcodec\" value=\"mp2v\"> MPEG 2<br>\n");
              print ("<input type=\"radio\" name=\"vcodec\" value=\"WMV2\"> WMV 2</td>\n");

              /* Show Bandwidth Options */
              print ("<td bgcolor=\"#E7C69A\">");
              print ("<input type=\"radio\" name=\"bw\" value=\"1\">  256 kbps<br>\n");
              if ($is_local) {
                 /* If the access is made from the local subnet, offer additional bandwidth options and do not preselect anything. */
                 print ("<input type=\"radio\" name=\"bw\" value=\"2\">  384 kbps<br>\n");
                 print ("<input type=\"radio\" name=\"bw\" value=\"3\">  448 kbps<br>\n");
                 print ("<input type=\"radio\" name=\"bw\" value=\"4\">  544 kbps<br>\n");
                 print ("<input type=\"radio\" name=\"bw\" value=\"5\"> 1152 kbps</td>\n");
              } else {
                 /* If the access is made from a remote network, offer only bandwidth options until 544 kbps. Pre-select 384 kbps. */
                 print ("<input type=\"radio\" name=\"bw\" value=\"2\" checked>  384 kbps<br>\n");
                 print ("<input type=\"radio\" name=\"bw\" value=\"3\">  448 kbps<br>\n");
                 print ("<input type=\"radio\" name=\"bw\" value=\"4\">  544 kbps</td>\n");
              }

              /* Show Audio Stream Options */
              print ("<td bgcolor=\"#FCD8A8\">");
              print ("<input type=\"radio\" name=\"audio\" value=\"1\" checked> 1<br>\n");
              print ("<input type=\"radio\" name=\"audio\" value=\"2\"> 2<br>\n");
              print ("<input type=\"radio\" name=\"audio\" value=\"3\"> 3</td></tr>\n");
           }
           if ($is_local)
              /* If the client is local, show the pure streaming option and pre-select it. */
              print ("<tr><td bgcolor=\"moccasin\" colspan=\"3\"><input type=\"radio\" name=\"vcodec\" value=\"stream\" checked> Pure Streaming</td></tr>\n");
           print ("</table><br>\n");

           /* Show the 'Start' and 'Clear' button. */
           print ("<input type=\"hidden\" name=\"state\" value=\"2\">\n");
           print ("<input type=\"submit\" value=\"Start\" style=\"background-color:springgreen;font-weight:bold\">\n");
           print ("<input type=\"reset\" value=\"Clear\" style=\"background-color:lightcoral;font-weight:bold\">\n");
           print ("</form>");
           fclose ($mov_ptr);
        }
        break;
    }
  /* Close telnet session to VLC server process. */
  fclose ($log_ptr);
  fputs($vlc_ptr,"exit\n");
  fclose ($vlc_ptr) or die ("VLC Socket cannot be closed.");
?>

</body>
</html>

Ist alles richtig installiert, sollte man das PHP-Skript aufrufen und Videos wiedergeben können. Viel Spaß beim Schauen!

Posted on: 2010-08-12Gabriel Rüeck