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.)
<?php // Clears the cache and prevent unwanted output ob_clean(); @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 if(isset($_SERVER['HTTP_RANGE'])){ // Parse the range header to get the byte offset $ranges = array_map( 'intval', // Parse the parts into integer explode( '-', // 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) if(!$ranges[1]){ $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 header( sprintf( '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 while(true){ // Check if we have outputted all the data requested if(ftell($f) >= $ranges[1]){ break; } // Output the data echo fread($f, $chunkSize); // Flush the buffer immediately @ob_flush(); flush(); } } else { // It's not a range request, output the file anyway header('Content-Length: ' . $size); // Read the file @readfile($file); // and flush the buffer @ob_flush(); flush(); }
I have not yet implemented this yet, but looking at the code I see a potential problem in the “no range” case:
readfile($file);
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: http://teddy.fr/2007/11/28/how-serve-big-files-through-php/ )
So, the second part of the if, should use a $chunksize too, in my opinion.
Anyway thanks for the great code, very useful.
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])?
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
Differences:
Chrome does not do the first 2 byte request
Firefox does not use RANGE protocol
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.
Hi,
This works only for local files?
$file = “http://website.com/video.mp4”;
not working
It should only work with local files.
If website1.com is allowed to see/open files on website2.com is a whole different (security) problem. So http://website.com/index.html should have no problem to access http://website.com/video.mp4, and local index.html has no problem with local video.mp4, but there is a security lock to block access to http://website2.com/video.mp4 unless website2.com specifically allows it in complex ways (see https://jvaneyck.wordpress.com/2014/01/07/cross-domain-requests-in-javascript/ 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 http://website.com/video.mp4. But as soon as you upload the index page to the same website.com, it should start working.
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?
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]){
break;
}
Trying to solve this…
Thank you again for sharing this neat code.
Cheers,
Steven
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
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)
Thanks for this……………………. it’s relay great….
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.
Your PHP environment may not allow the
set_time_limit(0)
call, therefore, the script times out before it completes the request.Thanks. That was the issue. Now working fine. On a separate note in IE, file format is not recognised; doesn’t play.
Awesome! this script solved my problems. It works perfect on IE and Firefox but not in Chrome. Why?
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.
It’s a awesome post. Is there a way to protect audio/video file being downloaded?
You may check the referer header. If it’s absent or the URL value is not under your domain, you don’t transfer the file. Pretty simple 🙂
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!
I protect my video with .htaccess with the rule “Deny from all”. It makes impossible acces to the video directory but the php script.
Thanks for the code, Really helpful!
Also, I have three suggestion.
first:
Add things like:
ob_clean();
@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.
Second:
You can detect ‘mime type’ using ‘Fileinfo’ functions.
$finfo = new finfo(FILEINFO_MIME);
$mime = $finfo->file($file);
But may need some requirements.
Third:
Well, This part has no effect on your code…
if(session_status() == PHP_SESSION_NONE)
session_write_close();
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!
I don’t even think of taking care about sessions. Thanks for reminding me!
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?
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”;
I’m glad to hear that and thanks for pointing out that 😉 I don’t even know I’ve made a typo there.