Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5 video and partial range HTTP requests

Tags:

html

http

video

I'm trying to modify a custom web server app to work with HTML5 video.

It serves a HTML5 page with a basic <video> tag and then it needs to handle the requests for actual content.

The only way I could get it to work so far is to load the entire video file into the memory and then send it back in a single response. It's not a practical option. I want to serve it piece by piece: send back, say, 100 kb, and wait for the browser to request more.

I see a request with the following headers:

http_version = 1.1
request_method = GET

Host = ###.###.###.###:##
User-Agent = Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0
Accept = video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5
Accept-Language = en-US,en;q=0.5
Connection = keep-alive
Range = bytes=0-

I tried to send back a partial content response:

HTTP/1.1 206 Partial content
Content-Type: video/mp4
Content-Range: bytes 0-99999 / 232725251
Content-Length: 100000

I get a few more GET requests, as follows

Cache-Control = no-cache
Connection = Keep-Alive
Pragma = getIfoFileURI.dlna.org
Accept = */*
User-Agent = NSPlayer/12.00.7601.17514 WMFSDK/12.00.7601.17514
GetContentFeatures.DLNA.ORG = 1
Host = ###.###.###.###:##

(with no indication that the browser wants any specific part of the file.) No matter what I send back to the browser, the video does not play.

As stated above, the same video will play correctly if I try to send the entire 230 MB file at once in the same HTTP packet.

Is there any way to get this all working nicely through partial content requests? I'm using Firefox for testing purposes, but it needs to work with all browsers eventually.

like image 421
Eugene Smith Avatar asked Oct 24 '12 06:10

Eugene Smith


People also ask

What is partial HTTP request?

Partial request responsesA range request that is out of bounds will result in a 416 Requested Range Not Satisfiable status, meaning that none of the range values overlap the extent of the resource. For example, the first-byte-pos of every range might be greater than the resource length.

Why video is not playing in HTML5?

An 'HTML5: Video file not found' error indicates either the browser you are using doesn't support HTML5 or the webpage doesn't have the proper video codec. You may contact the website's developer to install HTML5 supporting codecs for all the three WebM, MP4, and OGG formats.

What is HTTP range?

HTTP range is an HTTP request header that is used to get part of a document from the server. If the server returns the part of the document, it uses the 206 (Partial Content) status code.

What is byte range request?

Byte-range requests occur when a client asks the server for only a portion of the requested file. The purpose of this is essentially to conserve bandwidth usage by avoiding the need to download a complete file when all that is required is a small section.


2 Answers

I know this is an old question, but if it helps you can try the following "Model" that we use in our code base.

class Model_DownloadableFile {
private $full_path;

function __construct($full_path) {
    $this->full_path = $full_path;
}

public function get_full_path() {
    return $this->full_path;
}

// Function borrowed from (been cleaned up and modified slightly): http://stackoverflow.com/questions/157318/resumable-downloads-when-using-php-to-send-the-file/4451376#4451376
// Allows for resuming paused downloads etc
public function download_file_in_browser() {
    // Avoid sending unexpected errors to the client - we should be serving a file,
    // we don't want to corrupt the data we send
    @error_reporting(0);

    // Make sure the files exists, otherwise we are wasting our time
    if (!file_exists($this->full_path)) {
        header('HTTP/1.1 404 Not Found');
        exit;
    }

    // Get the 'Range' header if one was sent
    if (isset($_SERVER['HTTP_RANGE'])) {
        $range = $_SERVER['HTTP_RANGE']; // IIS/Some Apache versions
    } else if ($apache = apache_request_headers()) { // Try Apache again
        $headers = array();
        foreach ($apache as $header => $val) {
            $headers[strtolower($header)] = $val;
        }
        if (isset($headers['range'])) {
            $range = $headers['range'];
        } else {
            $range = false; // We can't get the header/there isn't one set
        }
    } else {
        $range = false; // We can't get the header/there isn't one set
    }

    // Get the data range requested (if any)
    $filesize = filesize($this->full_path);
    $length = $filesize;
    if ($range) {
        $partial = true;
        list($param, $range) = explode('=', $range);
        if (strtolower(trim($param)) != 'bytes') { // Bad request - range unit is not 'bytes'
            header("HTTP/1.1 400 Invalid Request");
            exit;
        }
        $range = explode(',', $range);
        $range = explode('-', $range[0]); // We only deal with the first requested range
        if (count($range) != 2) { // Bad request - 'bytes' parameter is not valid
            header("HTTP/1.1 400 Invalid Request");
            exit;
        }
        if ($range[0] === '') { // First number missing, return last $range[1] bytes
            $end = $filesize - 1;
            $start = $end - intval($range[0]);
        } else if ($range[1] === '') { // Second number missing, return from byte $range[0] to end
            $start = intval($range[0]);
            $end = $filesize - 1;
        } else { // Both numbers present, return specific range
            $start = intval($range[0]);
            $end = intval($range[1]);
            if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1)))) {
                $partial = false;
            } // Invalid range/whole file specified, return whole file
        }
        $length = $end - $start + 1;
    } else {
        $partial = false; // No range requested
    }

    // Determine the content type
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $contenttype = finfo_file($finfo, $this->full_path);
    finfo_close($finfo);

    // Send standard headers
    header("Content-Type: $contenttype");
    header("Content-Length: $length");
    header('Content-Disposition: attachment; filename="' . basename($this->full_path) . '"');
    header('Accept-Ranges: bytes');

    // if requested, send extra headers and part of file...
    if ($partial) {
        header('HTTP/1.1 206 Partial Content');
        header("Content-Range: bytes $start-$end/$filesize");
        if (!$fp = fopen($this->full_path, 'r')) { // Error out if we can't read the file
            header("HTTP/1.1 500 Internal Server Error");
            exit;
        }
        if ($start) {
            fseek($fp, $start);
        }
        while ($length) { // Read in blocks of 8KB so we don't chew up memory on the server
            $read = ($length > 8192) ? 8192 : $length;
            $length -= $read;
            print(fread($fp, $read));
        }
        fclose($fp);
    } else {
        readfile($this->full_path); // ...otherwise just send the whole file
    }

    // Exit here to avoid accidentally sending extra content on the end of the file
    exit;
}
}

You then use it like this:

(new Model_DownloadableFile('FULL/PATH/TO/FILE'))->download_file_in_browser();

It will deal with sending part of the file or the full file etc and works well for us in this and lots of other situations. Hope it helps.

like image 165
Luke Cousins Avatar answered Sep 28 '22 10:09

Luke Cousins


I want partial range requests, because I'll be doing realtime transcoding, I can't have the file completely transcoded and available upon request.

For response which you don't know the full body content yet (you can't guess the Content-Length, live encoding), use chunk encoding:

HTTP/1.1 200 OK
Content-Type: video/mp4
Transfer-Encoding: chunked
Trailer: Expires

1E; 1st chunk
...binary....data...chunk1..my
24; 2nd chunk
video..binary....data....chunk2..con
22; 3rd chunk
tent...binary....data....chunk3..a
2A; 4th chunk
nd...binary......data......chunk4...etc...
0
Expires: Wed, 21 Oct 2015 07:28:00 GMT

Each chunk is send when it's available: when few frames are encoded or when the output buffer is full, 100kB are generated, etc.

22; 3rd chunk
tent...binary....data....chunk3..a

Where 22 give the chunk byte length in hexa (0x22 = 34 bytes), ; 3rd chunk is extra chunk infos (optional) and tent...binary....data....chunk3..a is the content of the chunk.

Then, when the encoding is finished and all chunks are sent, end by:

0
Expires: Wed, 21 Oct 2015 07:28:00 GMT

Where 0 means there not more chunks, followed by zero or more trailer (allowed header fields) defined in the header (Trailer: Expires and Expires: Wed, 21 Oct 2015 07:28:00 GMT are not required) to provide checksums or digital signatures, etc.

Here is the equivalent of the server's response if the file was already generated (no live encoding):

HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 142
Expires: Wed, 21 Oct 2015 07:28:00 GMT

...binary....data...chunk1..myvideo..binary....data....chunk2..content...binary....data....chunk3..and...binary......data......chunk4...etc...

For more information: Chunked transfer encoding — Wikipedia, Trailer - HTTP | MDN

like image 27
mems Avatar answered Sep 28 '22 09:09

mems