Justin T.
Justin T.

Reputation: 846

Why can't I cast an MP4 file served by PHP from outside of the document root?

Ok, I give up... Something very strange is going on and, after days of messing with this, I have to ask for help. I have a PHP script that serves an MP4 file from outside of the document root. This script works great, except for one very important (to me at least) detail: it will not give me the option to cast the content. On the same server, when I access an MP4 file that IS inside the document root, I load the page and when I click the three dots in the bottom right corner of the Chrome video player, I have the option to Download or Cast to my Chromecast. Using my script, I only have the option to Download, and I REALLLLY need to CAST! I have tweaked this so much that the headers output from either method are all but identical. Here is my code...

<?php
$file=$_GET['file'];

//validate
if($file=="." || $file==".."){$file="";}

$mediaRoot="../../../hostMedia";
$file=$mediaRoot . DIRECTORY_SEPARATOR . $file;
$file=str_replace('\\',"/",$file);

$filesize = filesize($file);

$offset = 0;
$length = $filesize;

// find the requested range
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);

$offset = intval($matches[1]);

$length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;

// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $offset . '-' . ($offset + $length-1) . '/' . $filesize);
header('Content-Type: video/mp4');
header('Content-Length: ' . $filesize);
header('Accept-Ranges: bytes');
header('Cache-Control: max-age=0');

// open file for reading
$file = fopen($file, 'r');

// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);

// populate $data with all except the last byte of the file
$numBytes=($filesize-1);
$dataLen=0;
while($dataLen<$numBytes){
    $lenGrab=($numBytes-$dataLen);
    if($lenGrab>(1024*2700)){$lenGrab=(1024*2700);}
    $data=fread($file, $lenGrab);
    print($data);
    $dataLen+=strlen($data);
}

// close file
fclose($file);
?>

A thousand "thank-you"s to whoever solves this one!

UPDATE

Ok, taking @Brian Heward's advice, I have spent countless hours making sure that the headers are ABSOLUTELY IDENTICAL!!! I was so sure it would work, but alas, it still fails to give me the option to cast. Here is my updated PHP...

<?php
session_start();

$accessCode=$_SESSION['accessCode'];
$file=$_GET['file'];

//handle injection
if($file=="." || $file==".."){$file="";}

if($accessCode=="blahblahblah8"){

    $mediaRoot="../../../hostMedia";
    $file=$mediaRoot . DIRECTORY_SEPARATOR . $file;
    $file=str_replace('\\',"/",$file);

    $filesize = filesize($file);

    $offset = 0;
    $length = $filesize;
    $lastMod=filemtime($file);

    if ( isset($_SERVER['HTTP_RANGE']) ) {
        // if the HTTP_RANGE header is set we're dealing with partial content
        $partialContent = true;
        // find the requested range
        preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
        $offset = intval($matches[1]);
        $length = (($matches[2]) ? intval($matches[2]) : $filesize) - $offset;
    } else {
        $partialContent = false;
    }


    if ( $partialContent ) {
        // output the right headers for partial content
        header('HTTP/1.1 206 Partial Content');
        header('Content-Range: bytes ' . $offset . '-' . ($offset + $length-1) . '/' . $filesize);
    }else{
        header('HTTP/1.1 200 OK');
    }

    // output the regular HTTP headers
    header('Content-Type: video/mp4');
    header('Content-Length: ' . $length);
    header('Accept-Ranges: bytes');
    header('ETag: "3410d79f-576de84c004aa"');
    header('Last-Modified: '.gmdate('D, d M Y H:i:s \G\M\T', $lastMod)); 

    // don't forget to send the data too
    $file = fopen($file, 'r');

    // seek to the requested offset, this is 0 if it's not a partial content request
    fseek($file, $offset);

    //populate $data with all except the last byte of the file
    $numBytes=($length);
    $dataLen=0;
    while($dataLen<$numBytes){
        $lenGrab=($numBytes-$dataLen);
        if($lenGrab>(1024*2700)){$lenGrab=(1024*2700);}
        $data=fread($file, $lenGrab);
        print($data);
        $dataLen+=strlen($data);
    }

    fclose($file);
}else{
    echo "You are not authorized to view this media.";
}

?>

If someone can get this thing to work, you are seriously a superhero!

FINAL UPDATE (for now...)

Well, after many, many hours of frustration, I had to abandon the approach and try something different. Luckily, there are usually more than one way to accomplish something, and I have found another way. I am hosting the .mp4 files inside the doc root in a folder protected using HTTP Basic Auth. Very similar to what I was trying to achieve and it is working for me. Thanks for your advice and direction!

Upvotes: 0

Views: 461

Answers (2)

gev
gev

Reputation: 1

This script might be what you're looking for, it handles video serving via PHP really well, Source

 <?php
// disable zlib so that progress bar of player shows up correctly
if(ini_get('zlib.output_compression')) {
    ini_set('zlib.output_compression', 'Off'); 
}
 
$folder = '.'; 
$filename = 'video.mp4';
$path = $folder.'/'.$filename;
 
// from: http://l...content-available-to-author-only...n.net/post/stream-videos-php/
if (file_exists($path)) {
    // Clears the cache and prevent unwanted output
    ob_clean();
 
    $mime = "video/mp4"; // The MIME type of the file, this should be replaced with your own.
    $size = filesize($path); // 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($path, '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($path);
 
        // and flush the buffer
        @ob_flush();
        flush();
    }
 
}
die();
 
?>

Upvotes: 0

Brian Heward
Brian Heward

Reputation: 556

Your headers are "all but identical" and there is the problem. Make them identical :P

Use the developer tools on your browser, (F12) and check the network headers each request is making. The most likely causes are the following lines I used on a similar project and you seem to be missing:

header('Content-Description: File Transfer');
header('Content-Disposition: inline; filename=' . basename($file));

alternately it might want

header('Content-Disposition: attachment; filename=' . basename($file));

Upvotes: 3

Related Questions