Patrice Ferlet
Patrice Ferlet
Créateur de ce blog.
Publié le juil. 22, 2010 Temps de lecture: 5 min

Me suivre sur Mastodon

Streaming PHP pour video ogv

Il est possible de streamer un flux avec PHP. En général on s’en sert pour la vidéo ou l’audio, mais c’est aussi très utile pour les clients qui savent gérer ce que l’on appelle un “resume download” (récupération d’un téléchargement coupé à certain endroit). Cela est aussi presque une obligation lorsque l’on veut utiliser la balise “video” en HTML5 et que le format n’est pas auto-streamé (les H264 par exemple n’ont pas ce souci) tel que le format libre Theora Ogg Vorbis.

Vous l’avez remarqué mais Youtube envoit les données vidéo au fur et à mesure que la lecture avance… et vous pouvez lire un film dés le milieur sans avoir à télécharger le début de la vidéo. C’est franchement intéressant mais pour cela il vous fallait un lecteur flash et soit un serveur de streaming, soit utiliser du H264 dont les entêtes du conteneur ont tout ce qu’il faut pour atteindre un point donné dans le fichier. Or j’aime utiliser le format Theora Ogg et il ne contient pas ces données. De plus, je n’aime pas flash et je voulais utiliser la balise video.

Si j’insert une balise vidéo directement, j’ai deux solutions: -laisser la balise de base, mais l’utilisateur ne pourra pas voir le film à partir d’un point donné sans passer par le chargement au moment du clique sur le bouton de lecture -ajouter le mot clef “preload” ou “autobuffer” pour charger le film à l’affichage de ma page, mais cela n’enlève pas qu’il faut charger toute la vidéo… donc même si l’utilisateur ne veut pas lire la vidéo, il aura le chargement depuis mon serveur…

Dans les deux cas, je ne stream pas, je charge le fichier sur le poste client. Mon serveur est donc forcé d’envoyer beaucoup de données, et le client voit sa bande passante diminuer le temps que tout se charge. En plus de cela, c’est peu pratique pour l’utilisateur…

Heureusement, le protocole HTTP est très souple et permet l’ajout d’entête. Les clients (je parle cette fois ci des logiciels, comme Firefox ou Chrome) peuvent alors envoyer des information pour dire “je veux commencer à tel endroit du fichier”. Et c’est avec ce genre d’entête (ajouté d’après la norme HTTP 1.1) que nous allons travailler.

Avant d’attaquer la pratique, nous allons devoir nous pencher sur 2 points: -ce qu’est un streaming (dans le fonctionnement) -la lecture des entêtes HTTP et les réponses à apporter

Un streaming est une méthode d’envoit de fichier par blocs. C’est un résumé simple mais pourtant vrai. L’idée est de répondre à une demande selon plusieurs points: -bande passante -taille de données à envoyer -à partir de quel position du fichier -etc…

Faire un stream résulte donc simplement à répondre par un lot de données à une demande spécifique plutôt que d’envoyer un lots complet.

HTTP a une valeur d’entête utilisable par le client pour définir quel lot est demandé. La variable s’appelle “RANGE”. Avec PHP, il est aisé de la lire: $range = $_SERVER['HTTP_RANGE'];

Si cette valeur existe alors le client demande un rang de données et non pas le lot complet. Le serveur devra alors répondre quel lot il peut envoyer (multi byte ou non, taille du lot, positions possibles…)

Voici les 3 entêtes qui sont les plus significatives: -Accept-Ranges qui prendra une valeur “X-Y” ou X est le début du rang possible (en général 0) et Y la taille du lot. -Content-Range qui défini le rang envoyé ou, en cas de demande malformé, le rang que le client doit demander. La forme est “bytes octet_de_début-octet_de_fin/taille_possible”. Attention, ce n’est pas une division mais bel et bien un format de réponse ! -Content-Length déjà bien connu, c’est la taille du lots d’octets envoyé

Très bien. Maintenant il faut savoir lire le rang demandé. -si le rang est de la forme X-Y alors on nous demande d’envoyer les données depuis l’octet X à l’octet Y -si le rang est de la forme X- on nous demande d’envoyer des données depuis X jusqu’au dernier octet possible -si le rand est de la forme -Y alors on nous demande de partir de Y octets **depuis la fin du fichier**

Il reste donc à traiter cela. Voici une petite fonction que j’ai écrit pour gérer les demandes de rang: ` sendRange($file) { $size = filesize($file);

//le type du fichier
header('Content-Type: '.mime_content_type($file));

//on ne nous demande pas de rang, on envoit le fichier
if(!isset($_SERVER['HTTP_RANGE'])){
    header('Content-Length: '.filesize($file));
    readfile($file);
    exit;
}

//on répond avec le rang possible
header("Accept-Ranges: 0-$size");

$range = $_SERVER['HTTP_RANGE'];

//on a un format "bytes=X-Y", je pars donc du caractère 6 et je coupe au "-"
$parts = explode('-', substr($range,6));
$start = $parts[0];

//le dernier octet se trouve à la taille du fichier - 1 octet (car on part de l'octet 0)
$end = (isset($parts[1]) && is_numeric($parts[1])) ? $parts[1] : $size-1;

//on ne m'a pas donné d'octet de début
//on me demande de partir d'un octet particulier à partir de la fin du fichier
if(strlen($start)<1){
    $start = $size-$end;
}


//tien le client s'est planté... je lui explique son cas
if ($end < $start) {
    header('HTTP/1.1 416 Requested Range Not Satisfiable');
    header('Content-Range: bytes '."$start-$end/$size");
    exit;
}

//maintenant, on peut envoyer les données
//il faut prévenir le client qu'on sait gérer des données partielles
header('HTTP/1.1 206 Partial Content');

//et on lit le fichier (en mode byte)
$fp = fopen($file,'rb');

//je me déplace à l'octet demandé
fseek($fp,$start);

//je calcul la longueur des données que je vais envoyer
$length = $end - $start + 1;

//je fais des lots de 8ko
$buffer = 1024*8;

header("Content-Type: ".mime_content_type($file));
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: ".$length);

//si le fichier est gros, faut pas qu'on arrête le scipt !
set_time_limit(0);       

//et je lit le fichier
//ftell me donne la position de mon pointeur de fichier
while(!feof($fp) && ($pos = ftell($fp)) <= $end) {
    if ($pos + $buffer > $end) {
        //et oui, le prochain coup je vais déborder, je réduit mon buffer
        $buffer = $end - $pos + 1;
    }
    //on envoit enfin les données
    echo fread($fp, $buffer); 
    flush(); ob_flush();
}              
fclose($fp);

} `

Résultat des courses, en utilisant cette fonction, je peux faire démarrer une vidéo à partir de n’importe quel endroit du fichier si le client le demande. Gain de bande passante, gain de convivialité… merci PHP :)

comments powered by Disqus