Stream videos to HTML5 video container using HTTP & PHP

Sometimes we need to feed videos dynamically from the server-side. If you’re feeding the video to a HTML5 <video> element, you may find that the video progress controls freezes and users cannot move it in any ways. (Thought this situation only happens in some browsers like Chrome and Firefox, the user experience hurts a lot.)

With a bit of investigation, I found out that Chrome requested the video with an HTTP range request which, the server-side handle it incorrectly and Chrome falls back to progressive downloading the video. With the little PHP script I wrote below, the server-side can now handle the HTTP range requests normally and the progress controls no longer freezes! (There’s one more benefit: fast forward and backward works much smoother in large video files.)

// Clears the cache and prevent unwanted output
@ini_set('error_reporting', E_ALL & ~ E_NOTICE);
@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 'Off');

$file = "/path/to/your/media/file"; // The media file's location
$mime = "application/octet-stream"; // The MIME type of the file, this should be replaced with your own.
$size = filesize($file); // The size of the file

// Send the content type header
header('Content-type: ' . $mime);

// Check if it's a HTTP range request
    // Parse the range header to get the byte offset
    $ranges = array_map(
        'intval', // Parse the parts into integer
            '-', // The range separator
            substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header

    // If the last range param is empty, it means the EOF (End of File)
        $ranges[1] = $size - 1;

    // Send the appropriate headers
    header('HTTP/1.1 206 Partial Content');
    header('Accept-Ranges: bytes');
    header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range

    // Send the ranges we offered
            'Content-Range: bytes %d-%d/%d', // The header format
            $ranges[0], // The start range
            $ranges[1], // The end range
            $size // Total size of the file

    // It's time to output the file
    $f = fopen($file, 'rb'); // Open the file in binary mode
    $chunkSize = 8192; // The size of each chunk to output

    // Seek to the requested start range
    fseek($f, $ranges[0]);

    // Start outputting the data
        // Check if we have outputted all the data requested
        if(ftell($f) >= $ranges[1]){

        // Output the data
        echo fread($f, $chunkSize);

        // Flush the buffer immediately
else {
    // It's not a range request, output the file anyway
    header('Content-Length: ' . $size);

    // Read the file

    // and flush the buffer

27 Replies to “Stream videos to HTML5 video container using HTTP & PHP”

  1. I have not yet implemented this yet, but looking at the code I see a potential problem in the “no range” case:


    reads the full contents of the file in memory, so it is not recommended for large files. It is usually replaced with a custom made “readfile_chunked”.

    (see this: )

    So, the second part of the if, should use a $chunksize too, in my opinion.

    Anyway thanks for the great code, very useful.

  2. Thank you for the post! Just a quick question/observation: Shouldn’t the content length be ($range[1] – $range[0] + 1) instead of ($range[1] – $range[0])?

    1. Yes indeed. Safari will request two bytes at first request, range[0]=0, range[1]=1. Then content length should be $range[1] – $range[0] + 1 = 2.

      Further, Safari can then request the full file, range : [0] to [size-1], and if content length is size – 1 – 0 (one byte short) Safari will continue by requesting the last byte for ever (until page is reloaded, and sometimes not stopping until the tab is closed.)

      Otherwise, the script works for me. PHP 7.0, Safari, Firefox, Chrome on MacOS. Versions as of Nov 7, 2016

      Chrome does not do the first 2 byte request
      Firefox does not use RANGE protocol

  3. Is there a possibility to check if all parts are transfered to the client?

    Or: Is it possible to identify the current streaming “session”?

    I’m currently working on a system which should protect the video from being downloaded by the user and I’ve been stuck on this problem for a while now.

    1. If is allowed to see/open files on is a whole different (security) problem. So should have no problem to access, and local index.html has no problem with local video.mp4, but there is a security lock to block access to unless specifically allows it in complex ways (see about text files, but same applies to all files).
      The same problem applies if a local page index.html tries to reach a remote file But as soon as you upload the index page to the same, it should start working.

  4. Hello,
    Great post about streaming videos from php. I got into this for a bit now and I’d like to add a subtitle track to video. Is that possible?

  5. hi Licson,

    Thank you for brilliant code.
    Works well, seeking VERY fast even with 5 hours footage.

    The only problem (in Chrome), is that streaming doesn’t end well, it just stops, after video completes just stops.
    Normal MP4, rewinds it to the begining.

    I think it has to do with they way code handles, end of file line 55:
    // Check if we have outputted all the data requested
    if(ftell($f) >= $ranges[1]){

    Trying to solve this…
    Thank you again for sharing this neat code.

    1. I think that is normal behavior and has nothing to do with the streamer. It is the role of the JS or the HTML to loop the video.
      The HTML should have a loop tag: … or JavaScript should listen to video ending event and restart it

  6. I have setup a ffmpeg and I’m streaming a file from localhost to rtmp server.
    What i need is to stream a file from the client PC.

    i.e., I am hosting ffmpeg

    on the server and i need my web application to streams the file from the client PC

    to the rtmp server. Is it possible?

    I don’t want to upload the entire file to my server because sometime the files are from a live feed (mixer studio outputs)

  7. I tried this a large (200mb file) MP4. It begins well (very quick) but dies after about ~ 40 seconds
    Then it stops. While it is playing I can seek any location but overall duration of playback remains about 40 seconds. Pl. advise.

      1. Thanks. That was the issue. Now working fine. On a separate note in IE, file format is not recognised; doesn’t play.

  8. hi,
    tried this with video streaming. Though it worked but wasn’t smooth(I have high speed connection). A lot of buffering was occurring. Any suggestion how to make it smooth streaming in case of video.

      1. help me, How to check the referer header. If it’s absent or the URL value is not under your domain then don’t transfer the file, give me example in code php please!

    1. I protect my video with .htaccess with the rule “Deny from all”. It makes impossible acces to the video directory but the php script.

  9. Thanks for the code, Really helpful!

    Also, I have three suggestion.
    Add things like:

    @ini_set('error_reporting', E_ALL & ~ E_NOTICE);
    @apache_setenv('no-gzip', 1);
    @ini_set('zlib.output_compression', 'Off');

    In the fist line, helps avoid data corruption.
    You can detect ‘mime type’ using ‘Fileinfo’ functions.

    $finfo = new finfo(FILEINFO_MIME);
    $mime = $finfo->file($file);

    But may need some requirements.
    Well, This part has no effect on your code…

    if(session_status() == PHP_SESSION_NONE)

    But some people forget to close the session, so they may have problem resuming or seeking the file, or even opening another page with same session.
    Add this before streaming starts.

    Thanks again!

  10. Hi, thanks for sharing a solution. I spent a day trying to understand why mp4 video was not playing on iPad and discovered that it should be delivered with header(‘Accept-Ranges: bytes’); otherwise iPad treats video as unsupported frmat. I installed H264 streaming module but I cannot see any way to force it stream my videos which are delivered to user via php script. Do you have any idea how can this be done without updating php?

  11. Hi,

    Thank you for that script! It saved my skin and worked right away briliantly 🙂

    There is also a little syntac error in your code.

    Line 3 is missing ; symbol at the end

    $mime = “application/octet-stream”;

Leave a Reply to Licson Lee Cancel reply

Your email address will not be published. Required fields are marked *