Handling HTTP Range Requests with PHP
Sometimes we don't want to load the entire file from the server, but only a certain section (range) of it. So we want to define a start byte and an end byte and return all data in that range. This is useful for partial downloads (= when we want to resume a download that has already started, or want to build download management via PHP) or in video streaming, when we don't want to load the whole video from the beginning but only the relevant part that is played next (or the part we just jumped to). We achieve this by creating a range request.
1. Request
For this we add the following header to the request:
Range: bytes=STARTBYTE-ENDBYTE
so that the server knows which section to deliver. This only works like this if we access the file directly, i.e. the server manages the download itself and handles the file. If the request has to pass through PHP first, we need to write our own script for it.
2. Server - Basics / reading the range
In the first step we have to find out which range should be returned. There are several ways to do that, but the simplest is to extract the range via regex.
if (isset($_SERVER['HTTP_RANGE'])) {
$range = preg_match('/^bytes=((\d*-\d*,? ?)+)$/', $_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : [0, filesize("video.mp4")];
}
$range[0] now contains the first byte we have to return. $range[1] contains the last byte we have to return. If the Range header wasn't set in the request, we return the whole file - so we start at byte 0 and end at the file size in bytes, which we can find out with filesize(). Now we still have to read the file, between the start byte and the end byte.
$fp = fopen('video.mp4', 'r');
fseek($fp, $range[0]);
echo fgets($fp, $range[1] - $range[0]);
Here we open the file into the variable "fp". fseek sets the cursor to the first byte position we have to return, and fgets returns the number of bytes we need to read. It's important that as the second parameter of fgets we don't pass the end byte itself, but the number of bytes to read (= end byte minus start byte). Now we can send a request with a header in the format
Range: bytes=startbyte-endbyte
3. Server - Advanced
The specification at tools.ietf.org describes further ways to request a specific range, though. The relevant part looks like this:
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
So to stay RFC compliant we also have to support the following formats:
Range: bytes=500-all bytes from byte 500 to the end of the fileRange: bytes=-500the last 500 bytes of the fileRange: bytes=50-99,200-249bytes 50-99 and 200-249
And all of that combined as well, e.g.
Range: bytes=0-99,500-1499,4000-bytes 0-99, 500-1499, 4000 to the end of the fileRange: bytes=0-99,500-1499,-200bytes 0-99, 500-1499 and the last 200 bytes of the file
The easiest way is a foreach loop.
$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]);
}
We then start reading the file from byte $parts[1][0] for a length of $parts[2][0] - $parts[1][0] bytes.
4. Notes
Keep in mind that we may also need to set a file header aka MIME type in the PHP file so that the browser knows which format it is. For an .mp4 file that would look like this, for example:
header("Content-Type: video/mp4");
header("Content-Length: " . filesize("video.mp4"));
You can find all MIME types on the selfHTML page.