Search code examples
javascriptphpgoogle-chromesafariwebkit

Skipping an MP3 causes player to break in Webkit browsers


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?


Solution

  • 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.