1

I have a PHP script that acts as a proxy between the user and the MP3. This is to prevent external embedding or hotlinking and so I can count plays of each MP3 file.

I am experiencing a problem though in Google Chrome and Safari (Opera, Firefox and Internet Explorer work perfectly) where it plays the MP3 fine and I can seek too but only a limited amount of times. After seeking a few times the player greys itself out (native chrome player) or displays an error (audio.js and JWPlayer 6). I can press play again and it will restart but audio.js wont show the play button again after such an error.

JWPlayer throws this error in the console:

MediaError
code: 2

__proto__: MediaError
MEDIA_ERR_ABORTED: 1
MEDIA_ERR_DECODE: 3
MEDIA_ERR_ENCRYPTED: 5
MEDIA_ERR_NETWORK: 2
MEDIA_ERR_SRC_NOT_SUPPORTED: 4

In chromes native player the console reports "Failed to load resource", My server error log shows no errors.

The code I am currently using is from Los Cherone's answer here: Make mp3 seekable PHP

My previous solution was this code which I am not using anymore, but for reference here it is:

<?php 
$track = '/public_html/mp3-previews/' . $_GET['stream'];

if(file_exists($track)) {
    header('Content-type: audio/mpeg');
header('Accept-Ranges: bytes');
    header('Content-length: ' . filesize($track));
    print file_get_contents($track);
} else {
    echo "no file";
}
?>

Both codes cause this error but it's worth pointing out that both codes stream audio and both of them allow me to seek a few times.

My initial thought was the function apache_request_headers was causing the problem because I am running PHP 5.3 and my host is running PHP as Fast CGI. So for Los Cherone's code I am using this workaround, but then why would my first attempt not work either?

EDIT: This is not the problem. I have updated my PHP version and this is still causing me issues.

Here is my hosted servers example: http://tabbidesign.com/audio/

A combination of Los Cherone's code and my own local Apache server with PHP 5.4.9 works perfectly, this problem does not occur. Here is the example: http://tabbicat.info/proving/audio/

What could be causing this?

Community
  • 1
  • 1
joshkrz
  • 499
  • 3
  • 7
  • 25
  • 1
    your code is saying it can accept byte-ranges, but utterly fails to actually serve up byte-ranges... It should be `Accept-ranges: none` – Marc B Feb 03 '14 at 21:55
  • 1
    don't load and serve the whole file after you trick the audio tag into asking for a sub-file range. – dandavis Feb 03 '14 at 22:03
  • @dandavis Turns out you were exactly right, I have used Fiddler to see what is going on and I get Content-Length mismatch. Could you please provide any pointers about where in the code is wrong. I have gone through it and researched the functions but as far as I can tell, it is just sending the bytes that are requested unless no range has been requested. I need the MP3 to be seekable. Thank you! – joshkrz Feb 04 '14 at 20:08
  • one thing i noticed is that your Content-Length header always says the full file size rather than the actual length of the response. the node server i know that works also emits "Connection:keep-alive", not sure which one matters. – dandavis Feb 04 '14 at 21:31

1 Answers1

2

I have managed to solve my issue.

After experimenting with other transfer methods, analysing my web traffic with Fiddler and taking some advice from the comments above I have made some changes to the code.

For some reason the script wasn't sending the correct amount of bytes in the loop shown below:

if ($start) fseek($fp,$start);
    while($length){
        set_time_limit(0);
        $read = 8192;
        $length -= $read;
        print(fread($fp,$read));
    }
    fclose($fp);

The reason why there were no problems in Firefox, Opera and IE was because they cache the whole file during playback so skipping backwards wasn't requesting the file again. Webkit (Chrome and Safari) requests the file again sending the content range every time.

$file_name  = '/home/sites/public_html/mp3-previews/' . $_GET['stream'];
stream($file_name, 'audio/mpeg');


function stream($file, $content_type = 'application/octet-stream') {
@error_reporting(0);

// Make sure the files exists, otherwise we are wasting our time
if (!file_exists($file)) {
    header("HTTP/1.1 404 Not Found");
    exit;
}

// Get file size
$filesize = sprintf("%u", filesize($file));

// Handle 'Range' header
if(isset($_SERVER['HTTP_RANGE'])){
    $range = $_SERVER['HTTP_RANGE'];
}elseif($apache = apache_request_headers()){
    $headers = array();
    foreach ($apache as $header => $val){
        $headers[strtolower($header)] = $val;
    }
    if(isset($headers['range'])){
        $range = $headers['range'];
    }
    else $range = FALSE;
} else $range = FALSE;

//Is range
if($range){
    $partial = true;
    list($param, $range) = explode('=',$range);
    // Bad request - range unit is not 'bytes'
    if(strtolower(trim($param)) != 'bytes'){ 
        header("HTTP/1.1 400 Invalid Request");
        exit;
    }
    // Get range values
    $range = explode(',',$range);
    $range = explode('-',$range[0]); 
    // Deal with range values
    if ($range[0] === ''){
        $end = $filesize - 1;
        $start = $end - intval($range[0]);
    } else if ($range[1] === '') {
        $start = intval($range[0]);
        $end = $filesize - 1;
    }else{ 
        // Both numbers present, return specific range
        $start = intval($range[0]);
        $end = intval($range[1]);
        if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1))))       $partial = false; // Invalid range/whole file specified, return whole file
    }
    $length = $end - $start + 1;
}
// No range requested
else $partial = false; 

// Send standard headers
header("Content-Type: $content_type");
header("Content-Length: $length");
header('Accept-Ranges: bytes');
//header('Connection: keep-alive');

//header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
//header('Pragma: no-cache'); // HTTP 1.0.
//header('Expires: 0'); // Proxies.

// send extra headers for range handling...
if ($partial) {

    header('HTTP/1.1 206 Partial Content');

    header("Content-Range: bytes $start-$end/$filesize");
    if (!$fp = fopen($file, 'rb')) {
        header("HTTP/1.1 500 Internal Server Error");
        exit;
    }
    if ($start) fseek($fp,$start);
        print(fread($fp,$length));
        fclose($fp);
}
//just send the whole file
else readfile($file);
exit;
}

I have sent the actual output size that is requested in the Content-Length header and I have removed the loop that sends the file in 8000 byte portions.

Now I am sure that if I tried to handle large files PHP will definitely run out of memory because I am handling the whole file instead of 8000 byte chunks, but I am only dealing with 4MB files each time so for now this is OK.

joshkrz
  • 499
  • 3
  • 7
  • 25
  • This answer helped me out immensely when trying to implement the Partial Content functionality of HTTP 1.1. The key for me was when you indicated that you set the `Content-Length` header to the size of the range that was requested, and not to the size of the file itself. I was setting Content-Length to the file size and could not figure out why Content-Range was resulting in a mismatch. – Harvtronix Mar 13 '15 at 05:07
  • Thanks so much. This was driving me nuts. It also solved a related problem I was having that Chrome would simply conk out when streaming long-ish (8 minutes+) MP3s served by PHP with a net::ERR_CONTENT_LENGTH_MISMATCH. I still get those errors in the Browser console, but it keeps playing. – frabjous Sep 04 '16 at 00:15