5

I'm attempting to download a .mp4 file. (about 1.3GB size). I'm using following:

<?php 
$path = "video.mp4";
header('Accept-Ranges: bytes');  // For download resume
header('Cache-Control: must-revalidate, post-check=0, pre-check=0' );
header('Content-Description: File Transfer' );
header('Content-Disposition: attachment; filename="'.basename( $path ).'"' );
header('Content-Length: ' . filesize($path));  // File size
header('Content-Transfer-Encoding: binary');  // For Gecko browsers mainly
header('Content-Type: application/octet-stream' );
header('Expires: 0' );
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($path)) . ' GMT');
header('Pragma: no-cache' );
ob_clean();
flush();
readfile($path);

I open my php file, and firefox pops up with the "want to save" menu. Size looks right. I press Save As, to my desktop. The final downloaded file, lists as a random size, around 400MB (330, 463 and 440).

Response Headers are:

Connection: Keep-Alive
Content-Disposition:    attachment; filename="//www.frederikspang.dk/elevgallavideo.mp4"
Content-Length: 1422778850
Content-Type:   video/mp4
Date:   Sun, 30 Jun 2013 22:12:30 GMT
Keep-Alive: timeout=10, max=50
Pragma: public
Server: Apache
content-transfer-encoding:  binary
Frederik Spang
  • 3,379
  • 1
  • 25
  • 43
  • 1
    You might be running up against an execution time limit. Does the download always stop after the same period of time? –  Jun 28 '13 at 21:08
  • Resumable file downloads takes a lot more code than this; so you should remove `Accept-Ranges`. See: http://stackoverflow.com/questions/157318/resumable-downloads-when-using-php-to-send-the-file – Rob W Jun 28 '13 at 21:09
  • Are you actually using output buffering? If not you probably don't need ob_clean() and flush() since the readfile command will output the data directly to the client. – dethtron5000 Jun 28 '13 at 21:21
  • I haven't timed it - @MikeW | It's not resumable, it's ment to be downloaded as one file, at once. – Frederik Spang Jun 28 '13 at 21:23
  • @dethtron5000 Even when I've not been using output buffering I've had trouble getting downloads to work reliably unless I clear the buffers first. It's a technique that's always worked for me. –  Jun 28 '13 at 21:26
  • I put set_time_limit(0) in the top of the script. No result. It stops after some time. Doesn't finish. – Frederik Spang Jun 28 '13 at 21:34
  • @FrederikSpang: have you tried `ob_end_clean()` or `ob_end_flush()`? These turns off the output buffering which might be causing you trouble. – BudwiseЯ Jun 30 '13 at 10:12
  • Will do ASAP. I'm still thinking execution time.. – Frederik Spang Jun 30 '13 at 11:32
  • @FrederikSpang: I'm pretty sure it has nothing to do with the script execution time. When the script arrives to the `readfile` line, AFAIK the execution is done. – BudwiseЯ Jun 30 '13 at 16:23
  • Apache execution time? Seeing Apache is still running when the file is sent. Top of my head. – Frederik Spang Jun 30 '13 at 16:44
  • Have you looked at the end of the downloaded file (or in your error logs) to check for error messages? Just about everything that would cause a file to only download part way would throw a PHP error. What I usually do with just about any issue as turn on error reporting to E_ALL and display_errors to on (ini_set('display_errors', 'on'); error_reporting(E_ALL);) and then just look at the end to see if an error was thrown (it probably was). – Chelsea Urquhart Jun 30 '13 at 17:58
  • @Chelsea - Doing so now. Also added `ob_end_clean()` before `readfile()`, @budwiser. – Frederik Spang Jun 30 '13 at 18:12
  • All of the file is in hex. Can't read any of it. ob_end_clean() didn't do any difference. Still ends at a random filesize. – Frederik Spang Jun 30 '13 at 18:16
  • Update. After listening to the response and request headers - I've found that filesize() is returning the wrong size. Will update whenever I get something. – Frederik Spang Jun 30 '13 at 21:59
  • No problem with the length. My fault. Updated question with response headers. – Frederik Spang Jun 30 '13 at 22:11

2 Answers2

2

THis is hard - most php configuration will fail after 30 seconds. If you own php.ini you can change that to longer limit. But still - is that even worth it? I mean - the files can get bigger or network slower - and once more you will hit the timeout.

This is why downloaders were made - to download big files in smaller chunks Half Crazed showed you code for that i THIS answer (its not only one - this only takes into account one of the ways clients negotiate the transfers - but still its a good start).

Mega.co.nz for example uses new html5 features. Downloads the file in browser using chunks, joining the file on user and and then ,,downloading'' it from the browser disk space. It can resume files, pause files and so on. (Sorry - no code for that as it would be quite big and include more than one language (php, js)).

PS: change yours readfile($path); into:

$handle=fopen($path, 'rb');
while (!feof($handle))
{
    echo fread($handle, 8192);
    flush();
}
fclose($handle);

This will not load WHOLE file into memory, just parts of 8KiB at once and then send them to user.

Community
  • 1
  • 1
Seti
  • 2,169
  • 16
  • 26
  • Although this is very true - It was the Apache2's Keep-Alive, that wasn't high enough. (Just saw this question again, thought I'd give you an answer) – Frederik Spang Aug 17 '14 at 08:19
  • Sorry for late answer, but keep-alive is for other reasons used... Most servers will just kill php after 60seconds - even if its still sending file (would be just killed). And lifting that limit os not often wise – Seti Sep 08 '17 at 10:15
0
   <?php
$filename = "theDownloadedFileIsCalledThis.mp4";
$myFile = "/absolute/path/to/my/file.mp4";

// Add bellow code for mime type
$ext=strtolower(substr($fl,strrpos($myFile,".")));
$mime_types = array(
            '.txt' => 'text/plain',
            '.htm' => 'text/html',
            '.html' => 'text/html',
            '.php' => 'text/html',
            '.css' => 'text/css',
            '.js' => 'application/javascript',
            '.json' => 'application/json',
            '.xml' => 'application/xml',
            '.swf' => 'application/x-shockwave-flash',
            '.flv' => 'video/x-flv',

            // images
            '.png' => 'image/png',
            '.jpe' => 'image/jpeg',
            '.jpeg' => 'image/jpeg',
            '.jpg' => 'image/jpeg',
            '.gif' => 'image/gif',
            '.bmp' => 'image/bmp',
            '.ico' => 'image/vnd.microsoft.icon',
            '.tiff' => 'image/tiff',
            '.tif' => 'image/tiff',
            '.svg' => 'image/svg+xml',
            '.svgz' => 'image/svg+xml',

            // video
            '.3gp' => 'video/3gpp',
            '.3g2' => 'video/3g2',
            '.avi' => 'video/avi',
            '.mp4' => 'video/mp4',
            '.asf' => 'video/asf',
            '.mov' => 'video/quicktime',
        );

if (array_key_exists($ext, $mime_types)){
   $mm_type=$mime_types[$ext];
}else{
   $mm_type="application/octet-stream";
}
$mm_type="application/octet-stream";

header("Cache-Control: public, must-revalidate"); // Avoid this line
header("Pragma: public"); // Add this line
header("Pragma: hack"); // Avoid this line
header("Content-Type: " . $mm_type);
header("Content-Length: " .(string)(filesize($myFile)) ); // Avoid this line
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-Length: ' . filesize($myFile)); // Add this line
header("Content-Transfer-Encoding: binary\n");
ob_clean(); // Add this line

readfile($myFile);

?>
Sachin Shukla
  • 1,249
  • 14
  • 17
  • 1
    Very unclear answer. It isn't documented well. Please elaborate "avoid" and "add", and why. – Frederik Spang Jun 30 '13 at 21:51
  • reference of mime types is useful and header declarations make sense... "Avoid" obviously means "don't use" and "Add" means "add". What he could have done was provided a better explanation as to why – zgr024 Oct 24 '13 at 12:57
  • What's with all the mime type garbage if you're just going to set `$mm_type="application/octet-stream"` anyways? – Wes Johnson Jan 23 '14 at 00:11