Traiter une requête HTTP Range avec PHP
Parfois, nous ne voulons pas charger tout le fichier depuis le serveur, mais seulement un certain extrait (Range) du fichier. Nous voulons donc définir un octet de début et un octet de fin et renvoyer toutes les données de cette plage. C'est utile pour les téléchargements partiels (= quand on veut reprendre un téléchargement déjà commencé ou développer une gestion de téléchargements via PHP) ou aussi dans le domaine du streaming vidéo, quand on ne veut pas charger toute la vidéo depuis le début mais seulement la partie pertinente, qui sera lue ensuite (ou la partie vers laquelle on vient de sauter). Nous y parvenons en créant une requête Range.
1. Requête
Pour cela, nous ajoutons à la requête l'en-tête suivant :
Range: bytes=STARTBYTE-ENDBYTE
afin que le serveur sache quelle portion livrer. Cela ne fonctionne toutefois ainsi que si nous accédons directement au fichier, c'est-à-dire si le serveur gère lui-même le téléchargement et traite le fichier. Si le tout doit auparavant passer par PHP, il faut écrire son propre script.
2. Serveur - Bases / lire la plage
Dans un premier temps, nous devons déterminer quelle plage doit être renvoyée. Il y a plusieurs possibilités, mais la plus simple est d'extraire la plage par expression régulière.
if (isset($_SERVER['HTTP_RANGE'])) {
$range = preg_match('/^bytes=((\d*-\d*,? ?)+)$/', $_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : [0, filesize("video.mp4")];
}
$range[0] contient désormais le premier octet que nous devons renvoyer. $range[1] contient le dernier octet que nous devons renvoyer. Si l'en-tête Range n'a pas été défini dans la requête, nous renvoyons le fichier entier, donc nous commençons à l'octet 0 et finissons à la taille du fichier en octets, que nous obtenons avec filesize(). Il faut maintenant lire le fichier, entre l'octet de début et l'octet de fin.
$fp = fopen('video.mp4', 'r');
fseek($fp, $range[0]);
echo fgets($fp, $range[1] - $range[0]);
Ici, nous ouvrons le fichier dans la variable « fp ». fseek place le curseur à la première position d'octet que nous devons renvoyer, et fgets renvoie le nombre d'octets à lire. Il est important de ne pas passer comme second paramètre de la fonction fgets l'octet de fin lui-même, mais le nombre d'octets à lire (= octet de fin moins octet de début). Nous pouvons maintenant envoyer une requête avec un en-tête au format
Range: bytes=Startbyte-Endbyte
3. Serveur - Avancé
La spécification sur tools.ietf.org décrit toutefois d'autres façons de demander une plage précise. La partie correspondante ressemble à ceci :
The first 500 bytes:
Range: bytes=0-499
The second 500 bytes:
Range: bytes=500-999
All bytes except for the first 500 until the end of document:
Range: bytes=500-
The last 500 bytes of the document:
Range: bytes=-500
Two separate ranges:
Range: bytes=50-99,200-249
The first 100 bytes, 1000 bytes starting from the byte number 500,
and the remainder of the document starting from byte number 4000:
Range: bytes=0-99,500-1499,4000-
The first 100 bytes, 1000 bytes starting from the byte number 500,
and the last 200 bytes of the document:
Range: bytes=0-99,500-1499,-200
Nous devons donc aussi prendre en charge les formats suivants pour rester conformes à la RFC :
Range: bytes=500-Tous les octets à partir de l'octet 500 jusqu'à la fin du fichierRange: bytes=-500Les 500 derniers octets du fichierRange: bytes=50-99,200-249Les octets 50-99 et 200-249
Et le tout aussi combiné, par exemple
Range: bytes=0-99,500-1499,4000-Octets 0-99, 500-1499, 4000 jusqu'à la fin du fichierRange: bytes=0-99,500-1499,-200Octets 0-99, 500-1499 et les 200 derniers octets du fichier
Le plus simple est de passer par une boucle foreach.
$fp = fopen('video.mp4', 'r');
$filesize = filesize("video.mp4");
foreach (split(substr($_SERVER['HTTP_RANGE'], 0, 6), ',') as $range) {
preg_match_all("/(\d*)-(\d*)/", $range, $parts);
if ($parts[2][0] == "") $parts[2][0] = $filesize;
if ($parts[1][0] == "") {
$parts[1][0] = $filesize - $parts[2][0];
$parts[2][0] = $filesize;
}
fseek($fp, $parts[1][0]);
echo fgets($fp, $parts[2][0] - $parts[1][0]);
}
Nous commençons alors à lire le fichier à partir de l'octet $parts[1][0] sur une longueur de $parts[2][0] - $parts[1][0] octets.
4. Remarques
Il faut en outre noter que nous devons éventuellement définir un en-tête de fichier, alias type MIME, dans le fichier PHP pour que le navigateur sache de quel format il s'agit. Pour un fichier .mp4, cela donne par exemple :
header("Content-Type: video/mp4");
header("Content-Length: " . filesize("video.mp4"));
Tous les types MIME se trouvent sur la page de selfHTML.