VIDEO STREAMING

7. Umsetzung

7.1. Code-Beispiele

Das Einfügen eines Medienassets mittels der REST API fängt grundsätzlich mit der Erstellung eines UPLOAD.TOKEN‘s an. Dieser Token wird anschließend in der Datenbank abgespeichert und wartet auf seine Verwendung. Es kann ein Ablaufdatum als Parameter angegeben werden.

<?php
function insertToken()
{
  $query="INSERT INTO ".$this->tableName." SET media=:mediaid, value=:value,   type=:type, expired=:expired, created=:created"; $stmt = $this->conn->prepare($query);
  $this->mediaid=htmlspecialchars(strip_tags($this->mediaid));
  $this->value=htmlspecialchars(strip_tags($this->value));
  $this->type=htmlspecialchars(strip_tags($this->type));
  $this->expired=htmlspecialchars(strip_tags($this->expired));
  $this->created=htmlspecialchars(strip_tags($this->created));
  $stmt->bindParam(":mediaid", $this->mediaid);
  $stmt->bindParam(":value", $this->value);
  $stmt->bindParam(":type", $this->type); 
  $stmt->bindParam(":expired", $this->expired);
  $stmt->bindParam(":created", $this->created);
  if($stmt->execute())
  {
    return true;
  }
  return false;
}
?>

Quellcode 3: insertToken-Funktion des Token-Objekts

Die Struktur des Projekts lässt sich an dem Header einer der implementierten REST-Methoden erkennen. Neben der Konfiguration werden die drei Objekte Token, Media sowie Xcdr geladen und jeweils mit einer Verbindung zur Datenbank ausgestattet.

<?php
header("Content-type: application/json; charset=utf-8");
include_once '../config/config.php';
include_once APIROOT.'/config/database.php';
include_once APIROOT.'/objects/token.php';
include_once APIROOT.'/objects/media.php';
include_once APIROOT.'/objects/xcdr.php';
umask(0);
$database = new Database();
$db = $database->getConnection();
$token = new Token($db);
$media = new Media($db);
$xcdr = new Xcdr($db);
?>

Quellcode 4: Header der Upload-Methode (‹/media/upload.php›)

Der XHR-Upload schickt den Token in einer anderen Datenstruktur an die Methode als ein üblicher HTML-Upload. Diese Datenstruktur wurde mit dem ersten Befehl auf die tatsächlichen POST-Variablen runtergebrochen und der Token wird im Fall eines XHR-Uploads ebenfalls erkannt und korrekt verifiziert.

<?php
// XHR-Upload
$zweivierzwei=json_decode(json_encode((array)$_POST),true);
// HTML-Upload
if (isset($_POST["token"]))
{
  $token->value=trim(htmlspecialchars(strip_tags($_POST["token"])));
  $token->type=1;
}
else if (isset($zweivierzwei['token']))
{
  $token->value=trim(htmlspecialchars(strip_tags($zweivierzwei['token']))); 
  $token->type=1;
}
else exit('{ "status" : "error", "message": "No token provided" }');
if (!$token->getTokenByKey())
  exit('{ "status" : "error", "message" : "Invalid token", "token" : "'.$token->value.'" }');
else
{
  if ($token->expired) exit('{ "status" : "error", "message" : "Token is expired", "token" : "'.$token->value.'" }');
}
?>

Quellcode 5: Verifizierung des Tokens mittels HTTP Multipart Upload oder XHR-Upload (‘/media/upload.php‘)

Eine XHR-Anfrage in Javascript an eine REST-Methode hinter einer HTTP Basic Auth-Passwortabfrage. Die Logindaten werden vom User in die usern– und passw-Texteingabefelder auf der Loginseite des Webuploaders eingefügt.

<script type="text/javascript">
function getToken()
{
  var usern = $("input#username").val();
  var passw = $("input#password").val();
  $.ajax({ url: "https://api.xcdr.org/token/publish.php", crossDomain: true, beforeSend: function(xhr) { xhr.setRequestHeader("Authorization", "Basic " + encodeBase64(usern + ":" + passw)) }, success: function(result) { 
  console.log("UPLOAD.TOKEN: "+result.token+"\n");
  document.getElementById('uploadToken').value=result.token; 
  document.getElementById('Logon').style['display'] = 'none'; 
  document.getElementById('viduploader').style['display'] = 'block'; } });
}
</script>

Quellcode 6: XHR-Anfrage des Webclients an die REST API (Javascript)

Die letzte Checkliste nach der Aufbereitung der Daten für das Xcdr-Objekt und anschließend für den Jobserver, der die Transkodierung unter der Job-Funktion „xcdr“ ausführt. Dieser Transcoding-Job ruft hiernach eine externe Funktion zur Messung der Bitrate auf dem Webserver, auf dem das generierte Medienasset geschrieben wurde, aus.

<?php
if ($xcdr->loadProfiles())
  if ($xcdr->generateJobParams())
    if ($jobclient=new GearmanClient())
      if ($jobclient->addServer())
      {
        $jobclient->doBackground("xcdr",$xcdr->jobParams);
        exit($response);
      }
?>

Quellcode 7: Letzte Checkliste vor der Transkodierung (‹/media/upload.php›)

Die Generierung der FFmpeg-Parameter ist das Herzstück des Encoders. Die Funktion unterscheidet primär zwischen Audio- und Videoasset und befolgt demnach geringe Abweichungen in der Syntax in Hinsicht auf das Zielmedium. Es wird nicht zwischen weiteren Medientypen, wie 360°-Filme usw. unterschieden, sondern allein zwischen einer Audiospur und einer Videospur. Diese Parameter werden anschliessend an den Jobserver geschickt, der die Encodierung des Videos ins HLS-Zielformat vornimmt.

<?php
foreach ($this->profiles as $i=>$thisProfile)
{
  if ($this->audioVideo=='video')
  {
    $this->ffParams.=' -lavfi "[0:v]fps='.$thisProfile["videoFramerate"].'[lines];
    [lines]scale=-2:'.$thisProfile["videoLines"];
    if(isset($this->subtitlesfile) && $thisProfile["videoLines"]>=240)
      $this→ffParams.='[subs];[subs]subtitles='.$this→subtitlesfile;
    $this->ffParams.='[final]" -map "[final]" -c:v '.$thisProfile["videoCodec"].' -profile:v '.$thisProfile["videoProfile"]; $this->ffParams.=' -level '.$thisProfile["videoLevel"].' -crf '.$thisProfile["videoCrf"].' -r '.$thisProfile["videoFramerate"];
    $this->ffParams.=' -g '.$thisProfile["videoGop"].' -pix_fmt yuv420p';
  } 
  $this->ffParams.=' -map 0:a? -c:a '.$thisProfile["audioCodec"];
  $this->ffParams.=' -b:a '.$thisProfile["audioBitrate"].' -r:a '.$thisProfile["audioSamplerate"]; $this->ffParams.=' -strict -2 -ac '.$thisProfile["audioChannels"]; $this->ffParams.=' -hls_list_size 2147483647 -hls_playlist_type vod -hls_allow_cache 1 -hls_segment_filename ';
  if ($this->audioVideo=="video")
  {
    $profiledir=$this->mediadir.'/'.$thisProfile["videoLines"]; 
    $profilejobdir=$thisProfile["videoLines"];
    $this->ffParams.='"'.$profiledir.'/video'.$thisProfile["videoLines"].'p-%09d.ts" ';
    $this->ffParams.='-hls_base_url "'.$thisProfile["videoLines"].'/" ';
  }
  else
  {
    $profiledir=$this->mediadir.'/'.$thisProfile["audioBitrate"]; 
    $profilejobdir=$thisProfile["audioBitrate"];
    $this->ffParams.='"'.$profiledir.'/audio'.$thisProfile["audioSamplerate"].'Hz-%09d.ts" ';
    $this->ffParams.='-hls_base_url "'.$this->mediawebdir.'/'.$thisProfile["audioBitrate"].'/" ';
  }
  $this->profilejobdirs[]=$profilejobdir;
  if (!file_exists($profiledir))
    if(mkdir($profiledir, 0777, true))
      $this->ffParams.=$profiledir.'/index.m3u8';
    else
      die();
  }
?>

Quellcode 8: Generierung der FFmpeg-Parameter zur adaptiven Enkodierung der HLS-Streams (‘/objects/xcdr.php‘)

Wie schon geschildert, besteht das Einfügen von fertigen Webvideos in Webseiten seit der Produktreife der Media Source Extension aus einem einzigen <video>-Tag. Javascript-Entwickler haben sehr viel Spielraum bei der Entwicklung von individuell gestalteten Playern.

<body>
  <header class="frontpage">
    <div class="flowplayer is-splash frontpage" data-splash="true" data-hls-qualities="true" data-swf="/x/flowplayer/flowplayerhls.swf" data-aspect-ratio="16:9">
    <?php
      if ($player)
        echo '    <video>
       <source type="application/x-mpegurl" src="http://'.$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"].'master.m3u8"> 
    </video> ';
      else
        echo '    <img src="/x/images/control.jpg" alt="VODNIC|ENCODER" class="frontpage" border="0"> ';
    ?>
    </div>
  </header>
</body>

Quellcode 9: Playout durch <video>-Tag mit Vorschaubild (‘v.php‘)

Bootstrap.js ermöglicht das korrekte Skalieren des Playouts auf den gesamten Fensterbereich. Dadurch ist ebenfalls das reibungslose Einfügen der Inhalte per <iframe> in andere Webseiten und -frameworks möglich. Das Video mit Vorschaubild dominiert den Inhalt im Playout.

<style>
  body {
    text-align: center;
    background-color:#000;
  }
  .flowplayer {
    margin: 0 0 0 0;
    padding: 0 0 0 0;
    background-color:#000;
    background-image:url(<?php
      echo $image;
    ?>);
  }
  .frontpage {
    background-color: #000;
    background-size: auto 100%;
    background-repeat: repeat;
    -webkit-background-size: auto 100%;
    -webkit-background-repeat: repeat;
    -moz-background-size: auto 100%;
    -moz-background-repeat: repeat;
    -o-background-size: auto 100%;
    -o-background-repeat: repeat;
    width: 100%;
    height: 100vh;
    padding: 0 0 0 0;
    margin: 0 0 0 0;
    background-position: center;
    background-image:url(<?php
      echo $image;
    ?>);
  }
</style>

Quellcode 10: CSS-/Bootstrap-Styles für eine optimierte Bildschirmausgabe im ganzen Fensterbereich (CSS aus ‘v.php‘)

Die Konfigurationsdatei für den Webserver lässt alle URI‘s mit der vorgegebenen Syntax einen einheitlichen Player laden. Sonst werden keine PHP-Scripts vorgesehen und jeder Versuch, eines auszuführen, wirft einen 404-Fehler (not found).

server_name yokai.funken.tv;
location /
{
  # First attempt to serve request as file, then
  # as directory, then fall back to displaying a 404.
  try_files $uri $uri/ =404;
}
location ~ ^/v/[0-9a-f]+/$
{
  fastcgi_pass unix:/run/php/php7.0-fpm.sock;
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_param SCRIPT_NAME $fastcgi_script_name;
  rewrite ^ /x/players/v.php break;
}
location ~ \.php$
{
  return 404;
}

Quellcode 11: Webservereinstellung für den Playout (nginx-Konfigurationsdatei)

Die Implementation von CORS (Cross-Origin Resource Sharing) ermöglicht das Betreiben von externen JavaScript-Clients an der REST API sowie den XHR-Upload von Mediendateien. Dieselben Befehle ermöglichen die Konfiguration der Zugriffskontrolle von externen Playern, die einen HLS-Stream direkt vom Webserver abspielen wollen, was in der jetzigen Konfiguration (ohne CORS) nur durch den eingebauten Player auf dem Webserver möglich ist94.

server_name api.xcdr.org;
location / {
  # First attempt to serve request as file, then
  # as directory, then fall back to displaying a 404.
  try_files $uri $uri/ =404;
}
location ~* \.php$ {
  if ($request_method ~* "(GET|POST)") {
    add_header "Access-Control-Allow-Origin" "https://yve.xc.pl";
  }
  # Preflighted requests
  if ($request_method = OPTIONS )
  {
    add_header "Access-Control-Allow-Origin" "https://yve.xc.pl";
    add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD";
    add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept";
    return 200;
  }
  auth_basic "VODNIC";
  auth_basic_user_file /etc/nginx/.htpasswd;
  fastcgi_pass unix:/run/php/php7.0-fpm.sock;
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_param SCRIPT_NAME $fastcgi_script_name;
  client_max_body_size 2G;
}

Quellcode 12: REST API-Server Einstellungen für Crossdomain-Anfragen (CORS)95 und HTTP Basic Auth via SSL (nginx)

7.2. Webencoder-Test

FHD Transkodierungstest mit dem frei erhältlichen und nutzbaren Blender-Film „Big Buck Bunny“96 mittels URL-Upload, anschliessender Transkodierung und Bitratenmessung (vgl. Abbildung 14).

  1. Wie man anhand der Abbildung beobachten kann, steigt beim URL-Upload der Download des Transcoders und simultan der Schreibzugriff auf die Festplatte.
  2. Zuerst erfolgt die Quelldateianalyse, um die Parameter für die Enkodierung zu ermitteln. Gleich danach lastet FFmpeg praktisch sämtliche CPU Ressourcen des Transcoders aus, da die libx264-Bibliothek die Auslastung der Transkodierung auf mehrere Prozessorkerne parametrisieren kann.
  3. Gleichzeitig schreibt der Transcoder über ein Netzwerkdateisystem (in diesem Fall fuse-sshfs) das generierte HLS-Dateisystem direkt auf den Webstorage, was man an dem Upload des Transcoders (netout 1.17M) und dem Download des Webstorage (netin 1.17M) mit gleichzeitigem Schreibzugriff in derselben Bitrate auf die Festplatte des Webstorage erkennen kann.
  4. Nach abgeschlossener Enkodierung liegt das Medienasset unter einer statischen URI auf dem Dateisystem per HTTP-Protokoll zum Abruf bereit.
  5. Der RAM-Verbrauch im Transcoder betrug zwischen 3 und 3,5 GB während des Prozesses.
  6. Die Transcodierungsdauer eines 15-minütigen FHD-Films belief sich bei 14 Kernen auf 28 Minuten.
Abbildung 14: Datenströme zwischen Transcoder und Webserver

7.3. Playout-Test

Es wurde ein Playout-Test mit dem HLS-kompatiblen Safari-Browser und dem macOS Network Link Conditioner97 durchgeführt, um wechselnde Netzwerkbedingungen (Geschwindig-keit, Paketverlust) zu simulieren und das Verhalten des HLS-Streams am Beispiel des in Safari eingebauten HLS-Players zu testen (vgl. Abbildung 15).

a) Network Link Conditioner ― Netzwerkprofil: aus → Videoprofil: 1080p
b) Network Link Conditioner ― Netzwerkprofil: DSL → Videoprofil: 576p
c) Network Link Conditioner ― Netzwerkprofil: 4G → Videoprofil: 360p

Abbildung 15: Playout der adaptiven Bitraten bei wechselnden Empfangsbedingungen

Die erste Hardcopy in Abbildung 14 zeigt die Bildqualität bei unbeschränkter Bandbreite. Es ist ein feinkörniges Bild erkennbar, auch wenn die Vergrößerung des Ausdrucks auf viele Auflösungen schließen lässt. Das nächste Bild ist bei einer DSL-Bandbreite etwas grobkörniger. Bei der letzten Simulation einer 4G-Bandbreite ist die Bildkompression deutlich erkennbar.

Die angestrebte Funktionalität einer flüssigen Filmvorführung bei stark wechselnden Netzwerkbedingungen wurde erreicht. Allerdings hat sich nach einige Proben ein Problem mit der Qualität ergeben.

Aus den Tests des Encoders geht hervor, dass der automatische Wechsel zwischen den jeweiligen Bitraten Artefakte zutage legt. Diese Artefakte entstehen durch den Wechsel zwischen konsekutiven Segmentdateien aus verschiedenen Bitraten, die keine Keyframe am Dateianfang besitzen. Eine Keyframe ist notwendig, damit der Player Informationen über den gesamten Bildaufbau bekommt und dient zur Synchronisation der Video- und Audiospur zwischen sog. B-Frames, die nur Teilinformationen über das Bild enthalten.

Um eine Keyframe am Anfang eines chunks unterzubringen, muss sich das Keyframe-Intervall (Group of Pictures) mit der zeitlichen Länge der Datei synchronisieren. Um dieses Ziel zu erreichen, muss die Länge der Segmentdatei in Sekunden durch den Wert der Group of Pictures (GOP) teilbar sein. In diesem Sinne, wenn eine Segmentdatei jeweils 3 Sekunden lang ist, muss die Group of Pictures bei 30 Bildern pro Sekunde ein Nenner von 30 sein, also 5, 10, 15 oder 30. Durch diese arithmetische Gleichung wird die Keyframe, also das Vollbild, immer an den Anfang der Segmentdatei geschrieben. Um diese arithmetische Gleichung anzuwenden, müssen jeweils das videoGop- sowie das videoFrames-Feld der Profiles-Tabelle modifiziert werden – dann ist diese Bedingung erfüllt. Es muss weiterhin ein zusätzliches Feld für die Dauer der Segmentdatei in der Tabelle untergebracht werden, die als Parameter an FFmpeg übergeben wird. Die Dauer der einzelnen Segmentdateien muss in allen Profilen gleich sein, damit der Player die Bitraten tauschen kann. Deswegen ist dieser Wert notwendig, um bei der Anpassung des GOP-Wertes sowie der Zahl der Bilder pro Sekunde die Keyframe am Anfang der Segmentdatei zu schreiben. Dadurch können die optischen Artefakte beim Playout verschiedener Bitraten eingeschränkt werden.