HTTP Range Request mit PHP verarbeiten
Manchmal wollen wir nicht die gesamte Datei vom Server laden, sondern nur einen gewissen Ausschnitt (Range) der Datei. Wir möchten also ein Startbyte und ein Endbyte definieren und alle Daten aus diesem Bereich zurückgeben. Das ist sinnvoll bei partial Downloads (= wenn wir als einen bereits begonnenen Downloadvorgang fortsetzen wollen oder ein Downloadmanagement via PHP entwicklen wollen) oder auch im Bereich des Videostreamings, wenn wir nicht von Anfang an das gesamte Video laden wollen sondern nur den relevanten Teil, der als nächstes abgespielt wird (oder der Teil, an den gerade gesprungen wurde). Das erreichen wir, indem wir einen Range Request erstellen.
1. Request
Dazu hängen wir an den Request folgenden Header:
Range: bytes=STARTBYTE-ENDBYTE
sodass der Server weiß, welchen Abschnitt er ausliefern soll. Das geht so allerdings nur, wenn wir direkt auf die Datei zugreifen. Also der Server den Download selbst managt und die Datei verarbeitet. Wenn das ganze vorher noch PHP durchlaufen soll, müssen wir uns hierzu ein eigenes Skript schreiben.
2. Server - Grundlagen / Range abfragen
Dazu müssen wir im ersten Schritt herausfinden, welcher Bereich zurückgegeben werden soll. Dazu gibt es mehrere Möglichkeiten, die einfachste ist jedoch, den Range via Regex herauszufiltern.
if (isset($_SERVER['HTTP_RANGE'])) {
$range = preg_match('/^bytes=((\d*-\d*,? ?)+)$/', $_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : [0, filesize("video.mp4")];
}
$range[0] enthält jetzt den ersten Byte, den wir zurückgeben müssen. $range[1] enthält den letzten Byte, den wir zurückgeben müssen. Wenn der Range Header im Request nicht gesetzt wurde, geben wir die ganze Datei zurück - beginnen also bei Byte 0 und enden bei der Dateigröße in Bytes, welche wir mit filesize() herausfinden können. Jetzt müssen wir die Datei noch auslesen, und zwar zwischen dem Startbyte und dem Endbyte.
$fp = fopen('video.mp4', 'r');
fseek($fp, $range[0]);
echo fgets($fp, $range[1] - $range[0]);
Hier öffnen wir die Datei in die Variable "fp". fseek setzt dabei den Cursor an die erste Byteposition, welche wir zurückgeben sollen und fgets gibt die Anzahl an Bytes zurück, welche wir auslesen müssen. Wichtig ist also, dass wir als zweiten Parameter für die Funktion fgets nicht das Endbyte selbst übergeben, sondern die Anzahl an Bytes, welche gelesen werden soll (= Endbyte minus Startbyte). Jetzt können wir also einen Request mit einem Header im Format
Range: bytes=Startbyte-Endbyte
senden.
3. Server - Erweitert
Die Spezifikation unter tools.ietf.org beschreibt jedoch noch weitere Möglichkeiten, einen bestimmten Range anzufordern. Der entsprechende Teil sieht so aus:
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
Wir müssen also außerdem noch folgende Formate unterstützen, um RFC konform zu bleiben:
Range: bytes=500-Alle Bytes ab Byte 500 bis zum Ende der DateiRange: bytes=-500Die letzten 500 Bytes der DateiRange: bytes=50-99,200-249Die Bytes 50-99 und 200-249
Und das ganze auch noch kombiniert, wie z.B.
Range: bytes=0-99,500-1499,4000-Bytes 0-99, 500-1499, 4000 bis zum Ende der DateiRange: bytes=0-99,500-1499,-200Bytes 0-99, 500-1499 und die letzten 200 Bytes der Datei
Am einfachsten geht das ganze über eine foreach-Schleife.
$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]);
}
Wir beginnen dann die Datei ab Byte $parts[1][0] auszulesen für eine Länge von $parts[2][0] - $parts[1][0] Bytes.
4. Hinweise
Zu beachten ist weiterhin, dass wir ggfs. noch einen Fileheader aka MIME-Type in der PHP Datei setzen müssen, damit der Browser weiß um welches Format es sich handelt. Bei einer .mp4 Datei sieht das ganze dann z.B. so aus:
header("Content-Type: video/mp4");
header("Content-Length: " . filesize("video.mp4"));
Alle MIME-Types finden wir auf der Seite von selfHTML.