I want to stream remote file to user via php script.
Now I have 2 function.
function dl_file_resumable($file, $is_resume=TRUE, $type, $name, $length, $header)
{
//Gather relevent info about file
$size = remotefilesize($file);
$fileinfo = pathinfo($file);
//workaround for IE filename bug with multiple periods / multiple dots in filename
//that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
$filename = (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) ? preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1) : $fileinfo['basename'];
//check if http_range is sent by browser (or download manager)
if($is_resume && isset($_SERVER['HTTP_RANGE']))
{
list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
list($range, $extra_ranges) = explode(',', $range_orig, 2);
}
else
{
$range = '';
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
list($seek_start, $seek_end) = explode('-', $range, 2);
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)),($size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs
(intval($seek_start)),0);
//add headers if resumable
if ($is_resume)
{
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($size - 1))
{
header('HTTP/1.1 206 Partial Content');
}
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
}
//headers for IE Bugs (is this necessary?)
//header("Cache-Control: cache, must-revalidate");
//header("Pragma: public");
header('Content-Type: ' . $type);
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
//header('Content-Length: '.$length);
//open the file
$fp = fopen($file, 'rb');
//seek to start of missing part
fseek($fp, $seek_start);
//start buffered download
while(!feof($fp))
{
//reset time limit for big files
set_time_limit(0);
print(fread($fp, 1024*8));
flush();
ob_flush();
}
fclose($fp);
exit;
}
and
function remotefilesize($url, $user = "", $pw = "")
{
ob_start();
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
if(!empty($user) && !empty($pw))
{
$headers = array('Authorization: Basic ' . base64_encode("$user:$pw"));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
$ok = curl_exec($ch);
curl_close($ch);
$head = ob_get_contents();
ob_end_clean();
$regex = '/Content-Length:\s([0-9].+?)\s/';
$count = preg_match($regex, $head, $matches);
return isset($matches[1]) ? $matches[1] : "unknown";
}
I got this code by googling and modified it a little bit.
the problem is, I tried this script with remote rar file. But when the rar file downloaded, it shows error when Im tring to open it. "Unexpected end of archive".
Whats wrong with this script??
I am not expert in php, so I need clear answer from you. All help will be appreciate. Thanks :)
Problem solved!!
actually, in my real code, there is an echo in dl_file_resumable function..
this will result the echo being attach in downloaded file and change the size and the code of file.. I detect this problem using hex editor and saw the echo text exists in my rar source code..
Related
I'm using the following code, I adapted: http://ee.php.net/manual/en/function.fread.php#84115 to my code to allow download resume, it works great on firefox but doesn't work at all in chrome. If I try in chrome, the file is downloading, I pause it, wait a few minutes, but then when I resume it the download finishes instantly and the file is corrupted.
Any idea why?
function dl_file_resumable($file, $is_resume=TRUE)
{
//First, see if the file exists
if (!is_file($file))
{
die("<b>404 File not found!</b>");
}
//Gather relevent info about file
$size = filesize($file);
$fileinfo = pathinfo($file);
//workaround for IE filename bug with multiple periods / multiple dots in filename
//that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
$filename = (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) ?
preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1) :
$fileinfo['basename'];
$file_extension = strtolower($path_info['extension']);
$ctype='application/octet-stream';
//check if http_range is sent by browser (or download manager)
if($is_resume && isset($_SERVER['HTTP_RANGE']))
{
list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
list($range, $extra_ranges) = explode(',', $range_orig, 2);
}
else
{
$range = '';
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
list($seek_start, $seek_end) = explode('-', $range, 2);
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)),($size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//add headers if resumable
if ($is_resume)
{
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($size - 1))
{
header('HTTP/1.1 206 Partial Content');
}
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
}
//headers for IE Bugs (is this necessary?)
//header("Cache-Control: cache, must-revalidate");
//header("Pragma: public");
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
//open the file
$fp = fopen($file, 'rb');
//seek to start of missing part
fseek($fp, $seek_start);
//start buffered download
while(!feof($fp))
{
//reset time limit for big files
set_time_limit(0);
print(fread($fp, 1024*8));
flush();
ob_flush();
}
fclose($fp);
exit;
}
dl_file_resumable('/home/var/www/app/back/media/ready/5a58hGuRFR.tar');
I did not tried execute your code, but it looks like there is some mess with switch statement:
It does not have any meaning - if $ext is set at all, it will fall into one and only default section
It has wrong syntax - after default should be :
It has variable $new_name which isn't set anywhere
In the end, having these errors in reply, different browsers will handle them differently.
How do you get the size of a portion of a video? I want to get the size of one minute of a video. Is this possible?
I am making a php video streaming script and I realized that you cannot skip forward. This is what I plan to do: If a user skips forward one minute, I calculate the size of one minute of the video. Then I echo out the video, but skip the first minute.
This is my current code:
function readfile_chunked($filename, $retbytes = TRUE) {
$buffer = "";
$cnt = 0;
$handle = fopen($filename, 'rb');
if ($handle === false) {
return false;
}
while (!feof($handle)) {
$buffer = fread($handle, CHUNK_SIZE);
echo $buffer;
ob_flush();
flush();
if ($retbytes) {
$cnt += strlen($buffer);
}
}
$status = fclose($handle);
if ($retbytes && $status) {
return $cnt; // return num. bytes delivered like readfile() does.
}
return $status;
}
I also have a content-type header that I didn't include in the code above.
Thanks for all your help!
Its not how "skipping" is done.
What you should be doing is looking for the HTTP_RANGE headers that will be sent by the player.
Within your PHP you need to add and then handle the header('Accept-Ranges: bytes'); header.
So when a user clicks to skip forward in the video the player will send a $_SERVER['HTTP_RANGE'] to the server, you then use this to seek to the part of the file for output.
Here is and example (untested):
<?php
...
...
//check if http_range is sent by browser (or download manager)
if(isset($_SERVER['HTTP_RANGE'])){
list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($size_unit == 'bytes'){
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
list($range, $extra_ranges) = explode(',', $range_orig, 2);
}else{
$range = '';
header('HTTP/1.1 416 Requested Range Not Satisfiable');
exit;
}
}else{
$range = '';
}
//figure out download piece from range (if set)
list($seek_start, $seek_end) = explode('-', $range, 2);
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($file_size - 1) : min(abs(intval($seek_end)),($file_size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($file_size - 1)){
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$file_size);
header('Content-Length: '.($seek_end - $seek_start + 1));
}else{
header("Content-Length: $file_size");
}
header('Accept-Ranges: bytes');
...
...
?>
There is also a previous question here you may find usefull, which will be much easyier to implement.
I use PHP to send download to the browser. I run the code on a file named download.php.
The problem: sometimes instead of download the file, it's downloads the same file (same size), but with the name download.php.
I also noticed that when I try to download it with Internet Download Manager, I see the name download.php for about half second, and then the name changes to the real name.
Pictures to explain:
The code:
//First, see if the file exists
if (!is_file($file)) {
header("HTTP/1.1 400 Invalid Request");
die("<h3>File Not Found</h3>");
}
//Gather relevent info about file
$size = filesize($file);
$fileinfo = pathinfo($file);
//workaround for IE filename bug with multiple periods / multiple dots in filename
//that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
$filename = (isset($_SERVER['HTTP_USER_AGENT']) && strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) ?
preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1) :
$fileinfo['basename'];
$file_extension = strtolower($fileinfo['extension']);
//This will set the Content-Type to the appropriate setting for the file
switch($file_extension)
{
case 'exe': $ctype='application/octet-stream'; break;
case 'zip': $ctype='application/zip'; break;
case 'mp3': $ctype='audio/mpeg'; break;
case 'mpg': $ctype='video/mpeg'; break;
case 'avi': $ctype='video/x-msvideo'; break;
default: $ctype='application/force-download';
}
//check if http_range is sent by browser (or download manager)
if($is_resume && isset($_SERVER['HTTP_RANGE']))
{
$arr = explode('=', $_SERVER['HTTP_RANGE'], 2);
if(isset($arr[1]))
list($size_unit, $range_orig) = $arr;
else list($size_unit) = $arr;
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
$arr3 = explode(',', $range_orig, 2);
if(isset($arr3[1]))
list($range, $extra_ranges) = $arr3;
else
list($range) = $arr3;
}
else
{
$range = '';
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
$arr2 = explode('-', $range, 2);
if(isset($arr2[1]))
list($seek_start, $seek_end) = $arr2;
else
list($seek_start) = $arr2;
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)),($size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//add headers if resumable
if ($is_resume)
{
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($size - 1))
{
header('HTTP/1.1 206 Partial Content');
}
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
}
//headers for IE Bugs (is this necessary?)
//header("Cache-Control: cache, must-revalidate");
//header("Pragma: public");
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
//reset time limit for big files
set_time_limit(0);
ignore_user_abort(true);
//open the file
$fp = fopen($file, 'rb');
//seek to start of missing part
fseek($fp, $seek_start);
//start buffered download
while(!feof($fp))
{
print(fread($fp, 1024*8));
flush();
ob_flush();
}
fclose($fp);
exit;
I found the solution!
I did an htaccess RewriteRule. For example, if the file id is a1a2, and the file name is foo.mp4, I changed the URL to: download/a1a2/foo.mp4.
htaccess code:
RewriteEngine on
RewriteRule ^download/(.*)/(.*)?$ download.php?id=$1&fileName=$2 [QSA,NC,L]
That simple!
You can do this by setting proper headers as explained in the official PHP manual here:
// We'll be outputting a PDF
header('Content-type: application/pdf');
// It will be called downloaded.pdf
header('Content-Disposition: attachment; filename="downloaded.pdf"');
That said looking at your code it seems like you already the header stuff covered here:
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
So the only thing I think could be the issue is related to the list mime-types you have in your code:
//This will set the Content-Type to the appropriate setting for the file
switch($file_extension)
{
case 'exe': $ctype='application/octet-stream'; break;
case 'zip': $ctype='application/zip'; break;
case 'mp3': $ctype='audio/mpeg'; break;
case 'mpg': $ctype='video/mpeg'; break;
case 'avi': $ctype='video/x-msvideo'; break;
default: $ctype='application/force-download';
}
It could be that the mime-type (aka in your code as: $ctype) you are setting is not recognized by the browser or the IDM download software you are using. So it is defaulting to application/force-download.
I would also highly recommend refactoring your switch to work like this using simple associative array logic like this:
// This will set the Content-Type to the appropriate setting for the file
$ctype_array = array();
$ctype_array['exe'] = 'application/octet-stream';
$ctype_array['zip'] = 'application/zip';
$ctype_array['mp3'] = 'audio/mpeg';
$ctype_array['mpg'] = 'video/mpeg';
$ctype_array['avi'] = 'video/x-msvideo';
// Check if the file extension is in $ctype_array & return the value. If not, send default.
$ctype = array_key_exists($file_extension, $ctype_array) ? $ctype_array[$file_extension] : 'application/force-download';
That way you can easily add more items to the $ctype_array without having to juggle switch/case logic.
This question already has answers here:
PHP Remote file streaming with Resume Support
(3 answers)
Resumable downloads when using PHP to send the file?
(14 answers)
Closed 9 years ago.
I found this function on the internet:
function download_file($file,$ctype, $is_resume=TRUE)
{
//First, see if the file exists
if (!is_file($file))
{
die("<b>404 File not found!</b>");
}
//Gather relevent info about file
$size = filesize($file);
$fileinfo = pathinfo($file);
//workaround for IE filename bug with multiple periods / multiple dots in filename
//that adds square brackets to filename - eg. setup.abc.exe becomes setup[1].abc.exe
$filename = (strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE')) ?
preg_replace('/\./', '%2e', $fileinfo['basename'], substr_count($fileinfo['basename'], '.') - 1) :
$fileinfo['basename'];
//check if http_range is sent by browser (or download manager)
if($is_resume && isset($_SERVER['HTTP_RANGE']))
{
list($size_unit, $range_orig) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if ($size_unit == 'bytes')
{
//multiple ranges could be specified at the same time, but for simplicity only serve the first range
//http://tools.ietf.org/id/draft-ietf-http-range-retrieval-00.txt
list($range, $extra_ranges) = explode(',', $range_orig, 2);
}
else
{
$range = '';
}
}
else
{
$range = '';
}
//figure out download piece from range (if set)
list($seek_start, $seek_end) = explode('-', $range, 2);
//set start and end based on range (if set), else set defaults
//also check for invalid ranges.
$seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)),($size - 1));
$seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)),0);
//add headers if resumable
if ($is_resume)
{
//Only send partial content header if downloading a piece of the file (IE workaround)
if ($seek_start > 0 || $seek_end < ($size - 1))
{
header('HTTP/1.1 206 Partial Content');
}
header('Accept-Ranges: bytes');
header('Content-Range: bytes '.$seek_start.'-'.$seek_end.'/'.$size);
}
//headers for IE Bugs (is this necessary?)
//header("Cache-Control: cache, must-revalidate");
//header("Pragma: public");
header('Content-Type: ' . $ctype);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: '.($seek_end - $seek_start + 1));
//open the file
$fp = fopen($file, 'rb');
//seek to start of missing part
fseek($fp, $seek_start);
//start buffered download
while(!feof($fp))
{
//reset time limit for big files
set_time_limit(0);
print(fread($fp, 1024*8));
flush();
ob_flush();
}
fclose($fp);
exit;
}
The download is working, but when I use Download Manager (IDM Etc.) and I want to pause the download to continue it later, I can't. This function is supposed to allow that (look at the Content-Range header), but for some reason, it does not work.
Does anyone know why? And how do I fix it?
I've tried a lot of functions that I found on the internet, but nothing works properly.
#see https://github.com/audith/Persephone/blob/master/sources/handlers/static.php - starting from Line 658 for an example.
Suffice to say, resumable downloading requires correct HTTP header handling, specifically 'Connection: close' one wherever necessary.
Ok basically I have a project that requires that videos are hidden from the users while still able to see them (by using php). here's what i got so far:
The video.php file has this:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'path/to/movie.mp4');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$out = curl_exec($ch);
curl_close($ch);
header('Content-type: video/mp4');
header('Content-type: video/mpeg');
header('Content-disposition: inline');
header("Content-Transfer-Encoding: binary");
header("Content-Length: ".filesize($out));
echo $out;
exit();
?>
and the html file that is supposed to display this is using html5 as it would expect. now here's the thing.. when I straight embed this (not ) it works. but it doesn't work on my iPhone and doesn't work in the tag... if I use the direct file instead of the php wrapper, everything works fine, on my iPhone too...
so I guess my question for this one is this: what are the proper header() information to perfectly replicate an mp4 that can be streamed via iPhone and HMTL5?
Solution derived from: http://mobiforge.com/developing/story/content-delivery-mobile-devices
video.php file:
<?php
$file = 'path/to/videofile.mp4';
$fp = #fopen($file, 'rb');
$size = filesize($file); // File size
$length = $size; // Content length
$start = 0; // Start byte
$end = $size - 1; // End byte
header('Content-type: video/mp4');
header("Accept-Ranges: 0-$length");
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
if (strpos($range, ',') !== false) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
}else{
$range = explode('-', $range);
$c_start = $range[0];
$c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
}
$c_end = ($c_end > $end) ? $end : $c_end;
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $start-$end/$size");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
header('HTTP/1.1 206 Partial Content');
}
header("Content-Range: bytes $start-$end/$size");
header("Content-Length: ".$length);
$buffer = 1024 * 8;
while(!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
echo fread($fp, $buffer);
flush();
}
fclose($fp);
exit();
?>
Iphones use something called byte-ranges for audio and video requests. See this link for a solution. It's in Appendix A.
http://mobiforge.com/developing/story/content-delivery-mobile-devices
Here is a code snippet that will do what you want (from this question). The PHP solution seems more elegant, and it adds a more efficient solution that might work that uses the web server to serve the content.
<?php
$path = 'file.mp4';
$size=filesize($path);
$fm=#fopen($path,'rb');
if(!$fm) {
// You can also redirect here
header ("HTTP/1.0 404 Not Found");
die();
}
$begin=0;
$end=$size;
if(isset($_SERVER['HTTP_RANGE'])) {
if(preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
$begin=intval($matches[0]);
if(!empty($matches[1])) {
$end=intval($matches[1]);
}
}
}
if($begin>0||$end<$size)
header('HTTP/1.0 206 Partial Content');
else
header('HTTP/1.0 200 OK');
header("Content-Type: video/mp4");
header('Accept-Ranges: bytes');
header('Content-Length:'.($end-$begin));
header("Content-Disposition: inline;");
header("Content-Range: bytes $begin-$end/$size");
header("Content-Transfer-Encoding: binary\n");
header('Connection: close');
$cur=$begin;
fseek($fm,$begin,0);
while(!feof($fm)&&$cur<$end&&(connection_status()==0))
{ print fread($fm,min(1024*16,$end-$cur));
$cur+=1024*16;
usleep(1000);
}
die();
More Performance
Note that this is not the most efficient way to do it, because the whole file needs to go through PHP, so you will just need to try how it goes for you.
Assuming the reason you want to do this is to restrict access, and you need more efficiency later, you can use a flag for the web server.
Apache with X-Sendfile module or lightty (nginx info here)
$path = 'file.mp4';
header("X-Sendfile: $path");
die();
This is a bit more advanced and you should only use it if you need it, but it is relaxing to know you have an upgrade option when you start out with something that is rather easy but has mediocre performance.
This code was very handy for me, but I ran into trouble because I am using session vars, and PHP queues access to sessions. If a video was loading, all AJAX requests were impossible, etc. So make sure to call session_write_close() before you start output.
Yes, its easy to do. No need to set those headers manually. Let the server do it automatically.
Heres a working script -
ob_start();
if( isset($_SERVER['HTTP_RANGE']) )
$opts['http']['header']="Range: ".$_SERVER['HTTP_RANGE'];
$opts['http']['method']= "HEAD";
$conh=stream_context_create($opts);
$opts['http']['method']= "GET";
$cong= stream_context_create($opts);
$out[]= file_get_contents($real_file_location_path_or_url,false,$conh);
$out[]= $http_response_header;
ob_end_clean();
array_map("header",$http_response_header);
readfile($real_file_location_path_or_url,false,$cong);