Safari没有正确显示从php提供的mp3持续时间

时间:2014-03-06 01:42:41

标签: php zend-framework2 mp3

原始问题 我正在通过ZF2控制器操作提供mp3文件。这适用于除OS X和iPhone / iPad上的Safari之外的所有浏览器。 播放音频,但持续时间仅显示为NaN:NaN,而在其他浏览器中显示正确的持续时间。

我在SO上讨论了所有关于同样问题的线程,似乎它与响应头,特别是Content-Range和Accept-Ranges头有关。我尝试了所有不同的组合,但仍无济于事--Safari仍然拒绝正确显示持续时间。

相关的代码段如下所示:

$path = $teaserAudioPath . DIRECTORY_SEPARATOR . $teaserFile;
$fp = fopen($path, 'r');
$etag = md5(serialize(fstat($fp)));
fclose($fp);
$fsize = filesize($path);
$shortlen = $fsize - 1;

$response->setStatusCode(Response::STATUS_CODE_200);
$response->getHeaders()
            ->addHeaderLine('Pragma', 'public')
            ->addHeaderLine('Expires', -1)
            ->addHeaderLine('Content-Type', 'audio/mpeg, audio/x-mpeg, audio/x-mpeg-3, audio/mpeg3')
            ->addHeaderLine('Content-Length', $fsize)
            ->addHeaderLine('Content-Disposition', 'attachment; filename="teaser.mp3"')
            ->addHeaderLine('Content-Transfer-Encoding', 'binary')
            ->addHeaderLine('Content-Range', 'bytes 0-' . $shortlen . '/' . $fsize)
            ->addHeaderLine('Accept-Ranges', 'bytes')
            ->addHeaderLine('X-Pad', 'avoid browser bug')
            ->addHeaderLine('Cache-Control', 'no-cache')
            ->addHeaderLine('Etag', $etag);

$response->setContent(file_get_contents($path));

return $response;

播放器(我使用mediaelementjs)在Safari中看起来像这样: Note the NaN:NaN

我还尝试根据另一个示例解释HTTP_RANGE请求标头,如下所示:

        $fileSize = filesize($path);
        $fileTime = date('r', filemtime($path));
        $fileHandle = fopen($path, 'r');

        $rangeFrom = 0;
        $rangeTo = $fileSize - 1;
        $etag = md5(serialize(fstat($fileHandle)));
        $cacheExpires = new \DateTime();

        if (isset($_SERVER['HTTP_RANGE']))
        {
            if (!preg_match('/^bytes=\d*-\d*(,\d*-\d*)*$/i', $_SERVER['HTTP_RANGE']))
            {
                $statusCode = 416;
            }
            else
            {
                $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
                foreach ($ranges as $range)
                {
                    $parts = explode('-', $range);

                    $rangeFrom = intval($parts[0]); // If this is empty, this should be 0.
                    $rangeTo = intval($parts[1]); // If this is empty or greater than than filelength - 1, this should be filelength - 1.

                    if (empty($rangeTo)) $rangeTo = $fileSize - 1;

                    if (($rangeFrom > $rangeTo) || ($rangeTo > $fileSize - 1))
                    {
                        $statusCode = 416;
                    }
                    else
                    {
                        $statusCode = 206;
                    }
                }
            }
        }
        else
        {
            $statusCode = 200;
        }

        if ($statusCode == 416)
        {
            $response = $this->getResponse();

            $response->setStatusCode(416);  // HTTP/1.1 416 Requested Range Not Satisfiable
            $response->addHeaderLine('Content-Range', "bytes */{$fileSize}");  // Required in 416.
        }
        else
        {
            fseek($fileHandle, $rangeFrom);

            set_time_limit(0); // try to disable time limit

            $response = new Stream();
            $response->setStream($fileHandle);
            $response->setStatusCode($statusCode);
            $response->setStreamName(basename($path));

            $headers = new Headers();
            $headers->addHeaders(array(
                    'Pragma' => 'public',
                    'Expires' => $cacheExpires->format('Y/m/d H:i:s'),
                    'Cache-Control' => 'no-cache',
                    'Accept-Ranges' => 'bytes',
                    'Content-Description' => 'File Transfer',
                    'Content-Transfer-Encoding' => 'binary',
                    'Content-Disposition' => 'attachment; filename="' . basename($path) .'"',
                    'Content-Type' => 'audio/mpeg',  // $media->getFileType(),
                    'Content-Length' => $fileSize,
                    'Last-Modified' => $fileTime,
                    'Etag' => $etag,
                    'X-Pad' => 'avoid browser bug',
            ));

            if ($statusCode == 206) 
            {
                $headers->addHeaderLine('Content-Range', "bytes {$rangeFrom}-{$rangeTo}/{$fileSize}");
            }

            $response->setHeaders($headers);
        }

        fclose($fileHandle);

这仍然在Safari中给出​​了相同的结果。我甚至尝试使用核心PHP函数而不是ZF2 Response对象来渲染响应,使用h​​eader()调用和readfile(),但这也不起作用。

欢迎任何关于如何解决这个问题的想法。

修改 正如@MarcB所建议的,我比较了两个请求的响应头。第一个请求是提供mp3文件数据的PHP操作,第二个请求是直接浏览相同的mp3文件。起初标题并不完全相同,但我修改了PHP脚本以匹配直接下载的标题,请参阅下面的Firebug屏幕截图:

  1. PHP提供的响应标头: enter image description here

  2. 响应标头直接下载: enter image description here

  3. 正如你所看到的那样,除了Date标题之外它们完全一样,但那是因为请求之间只有一分半左右。当我尝试从PHP脚本提供文件时,Safari仍声称它是一个直播,因此当我以这种方式加载时,音频播放器仍然显示NaN的总时间。有没有办法告诉Safari只下载整个文件,当我说这不是直播时,请相信我?

    也可能是Safari发送不同的请求标头,因此响应标头也不同?我经常使用Firebug在Firefox中进行调试。例如,当我在Safari中打开mp3文件URL时,我无法打开Web Inspector对话框。有没有其他方法可以查看Safari发送和接收的标题?

    编辑2 我现在正在使用实现范围请求的简单流功能。即使在Safari中,这似乎也适用于我的开发机器,但在运行该网站的实时VPS服务器上却没有。

    我现在使用的功能(由另一个人提供,不记得确切的链接):

    private function stream($file, $content_type = 'application/octet-stream', $logger)
    {
        // Make sure the files exists, otherwise we are wasting our time
        if (!file_exists($file))
        {
            $logger->debug('File not found');
    
            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'];
            $logger->debug('Got Range: ' . $range);
        }
        elseif ($apache = apache_request_headers())
        {
            $logger->debug('Got Apache headers: ' . print_r($apache, 1));
    
            $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: $filesize");
        header('X-Pad: avoid browser bug');
        header('Accept-Ranges: bytes');
        header('Connection: Keep-Alive"');
    
        // 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);
            while ($length)
            {
                set_time_limit(0);
                $read = ($length > 8192) ? 8192 : $length;
                $length -= $read;
                print(fread($fp, $read));
            }
            fclose($fp);
        }
        // just send the whole file
        else
            readfile($file);
        exit();
    }
    

    然后在控制器操作中调用它:

            $path = $teaserAudioPath . DIRECTORY_SEPARATOR . $teaserFile;
            $fsize = filesize($path);
    
            $this->stream($path, 'audio/mpeg', $logger);
    

    我为调试目的添加了一些日志记录,差别似乎在请求标头中。在我的本地开发机器上,它在哪里工作,我在日志中得到它:

    2014-03-09T18:01:17-07:00 DEBUG (7): Got Range: bytes=0-1
    2014-03-09T18:01:18-07:00 DEBUG (7): Got Range: bytes=0-502423
    2014-03-09T18:01:18-07:00 DEBUG (7): Got Range: bytes=131072-502423
    

    在VPS上,它不起作用我得到了这个:

    2014-03-09T18:02:25-07:00 DEBUG (7): Got Range: bytes=0-1
    2014-03-09T18:02:29-07:00 DEBUG (7): Got Range: bytes=0-1
    2014-03-09T18:02:35-07:00 DEBUG (7): Got Apache headers: Array
    (
        [Accept] => */*
        [Accept-Encoding] => identity
        [Connection] => close
        [Cookie] => __utma=71101845.663885222.1368064857.1368814780.1368818927.55; _nsz9=385E69DA4D1C04EEB22937B75731EFEF7F2445091454C0AEA12658A483606D07; PHPSESSID=c6745c6c8f61460747409fdd9643804c; _ga=GA1.2.663885222.1368064857
        [Host] => <edited out>
        [Icy-Metadata] => 1
        [Referer] => <edited out>
        [User-Agent] => Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.73.11 (KHTML, like Gecko) Version/7.0.1 Safari/537.73.11
        [X-Playback-Session-Id] => 04E79834-DEB5-47F6-AF22-CFDA0B45B99F
    )
    

    在实时服务器上,只有前两个字节的初始请求(Safari用于确定服务器是否支持范围请求)进入(两次),但实际数据的范围请求永远不会完成。相反,我得到了一堆奇怪的请求标头,这些标头是由stream函数中的apache_request_headers()调用返回的。我没有在我的本地开发机器上获得它,它也运行Apache。

    任何想法都会非常感激,真的把我的头发拉出来。

3 个答案:

答案 0 :(得分:4)

今晚我花了一些时间讨论一个类似的问题 - 音频标签在大多数浏览器上运行正常,但不能在Safari上正确处理进度。我找到了一个适合我的解决方案,希望它也适合你。

我还阅读了有关类似问题的其他SO问题,他们都谈到了处理Range标题。有几个片段浮动,旨在处理Range标题。我在github上找到了一个简短的(ish)函数,它一直在为我工作。 https://github.com/pomle/php-serveFilePartial

我确实要对文件进行一次更改。在第38行:

header(sprintf('Content-Range: bytes %d-%d/%d', $byteOffset, $byteLength - 1, $fileSize));

我做了一个小修改(删除了-1)

header(sprintf('Content-Range: bytes %d-%d/%d', $byteOffset, $byteLength, $fileSize));

我刚刚向github发布了一个问题,解释了为什么我做了这个改变。我发现这个问题的方式很有意思:似乎Safari不信任服务器,因为它说它可以提供部分内容:Safari(或者我认为技术上的Quicktime)请求带有范围标题的文件的字节0-1,如下所示:

Range:  bytes=0-1

作为对文件的第一个请求。如果服务器返回整个文件 - 它将文件视为“流”,它没有开头或结尾。如果服务器使用该文件中的单个字节(和正确的标题)进行响应,则会询问该文件的几个不同范围(这看起来像是一种非常不合适的方式严重重叠)。我看到你已经注意到了这一点,并且你已经体验到Safari / Quicktime只发出第一个远程(0-1)请求,而没有后续的“真正的”远程请求。从我的讨论看来,这种情况正在发生,因为你的服务器没有提供“令人满意的”范围回复,所以它放弃了整个远程请求的想法。我在使用链接的serverFilePartial函数时遇到了这个问题,然后再进行调整。但是,在“修复”该行之后,Safari / Quicktime似乎对第一个响应感到满意,并继续进行后续的远程请求,并且进度条和所有内容都出现了,我们都很好。

所以,长话短说,给链接库一个去,看看它是否适合你,就像它对我一样:)我知道你已经找到了一个php解决方案,适用于你的开发机器而不是你的生产机器,但也许我的不同解决方案在您的VPS机器上会有所不同?值得一试。

答案 1 :(得分:1)

作为信息的补充,我相信这可能与此错误https://bugs.webkit.org/show_bug.cgi?id=82672

有关

提出了一些“解决方法”,如:

xhr.setRequestHeader("If-None-Match", "webkit-no-cache");

希望这可以帮助您或有类似问题的人。

答案 2 :(得分:0)

你自己通过javascript实例化播放器吗?只需尝试通过

加载资源
player.setSrc("http://www.xxx.de/controller/action");

并倾听'canplay' - 事件,如果你想直接玩:

player.addEventListener("canplay", function(){
     this.play();
});