Safari无法正确显示php提供的mp3持续时间 [英] Safari doesn't show duration of mp3 served from php correctly

查看:130
本文介绍了Safari无法正确显示php提供的mp3持续时间的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

原始问题 我正在从ZF2控制器操作中提供mp3文件.在OS X和iPhone/iPad上的Safari上,此功能在所有浏览器中均能正常运行. 会播放音频,但持续时间只是显示为NaN:NaN,而在其他所有浏览器中,则显示的是正确的持续时间.

Original question I'm serving an mp3 file from a ZF2 controller action. This works fine in all browsers except for Safari on OS X and iPhone/iPad. The audio plays, but the duration is just displayed as NaN:NaN, whereas in every other browser the correct duration is being displayed.

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

I went over all the threads on SO talking about the same problem and it seems like it has something to do with the response headers and the Content-Range and Accept-Ranges headers in particular. I've tried all the different combinations but still to no avail - Safari still refuses to display the duration correctly.

相关代码段如下所示:

$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中如下所示:

The player (I'm using mediaelementjs) looks like this in Safari:

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

I've also tried interpreting the HTTP_RANGE request header based on another example, like so:

        $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中,这仍然给我相同的结果.我什至尝试使用标头()调用和readfile()而不是ZF2 Response对象来使用核心PHP函数来呈现响应,但这也不起作用.

This still gives me the same result in Safari. I even tried using core PHP functions instead of the ZF2 Response object to render a response, using header() calls and readfile(), but that doesn't work either.

欢迎提出解决方案的任何想法.

Any ideas on how to solve this are welcome.

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

Edit As suggested by @MarcB I compared the response headers of the two requests. The first request is to the PHP action serving the mp3 file data and the second is when I browse to the same mp3 file directly. At first the headers weren't completely the same, but I modified the PHP script to match the headers of the direct download, see Firebug screenshots below:

  1. PHP提供的响应标头:

  1. Response headers served by PHP:

响应头直接下载:

您可以看到,除了Date标头外,它们完全相同,但这是因为请求之间存在大约一分半钟.当我尝试通过PHP脚本提供文件时,Safari仍然声称它是现场直播,因此当我以这种方式加载文件时,音频播放器仍显示总时间的NaN.有什么办法告诉Safari下载整个文件并在我说这不是现场直播时信任我吗?

As you can see they are exactly the same except for the Date header, but that's because there was about a minute and a half in between the requests. Still Safari is claiming it is a live broadcast when I try to serve the file from the PHP script and so the audioplayer still shows NaN for the total time when I load it that way. Is there any way to tell Safari to just download the whole file and just trust me when I say this is not a live broadcast?

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

Also could it be that Safari sends different request headers and thus the response headers are also different? I usually do my debugging in Firefox with Firebug. When I open the mp3 file URL in Safari for instance I cannot open the Web Inspector dialog. Is there any other way to view what headers are being sent and received by Safari?

编辑2 我现在使用一个简单的流函数来实现范围请求.即使在Safari中,这似乎也可以在我的开发机上正常运行,但在运行该站点的实时VPS服务器上却无法运行.

Edit 2 I'm now using a simple stream function implementing the range requests. This seems to work on my dev machine even in Safari, but not on the live VPS server where the site is running.

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

The function I use now (courtesy of another SO-er, don't remember the exact link):

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();
}

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

This is then called in the controller action:

        $path = $teaserAudioPath . DIRECTORY_SEPARATOR . $teaserFile;
        $fsize = filesize($path);

        $this->stream($path, 'audio/mpeg', $logger);

我添加了一些用于调试目的的日志记录,区别似乎在请求标头中.在我本地的开发机器上,在工作的地方,我在日志中得到了这个信息:

I added some logging for debugging purposes and the difference seems to be in the request headers. On my local dev machine, where it works I get this in the log:

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上,我得到以下提示:

On the VPS, where it doesn't work I get this:

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用来确定服务器是否支持范围请求(两次),但是从未完成对实际数据的范围请求.相反,我得到了流函数中apache_request_headers()调用返回的一堆奇怪的请求标头.我在运行Apache的本地开发机器上没有得到该信息.

Somehow on the live server only the initial request for the first two bytes, which Safari uses to determine if a server supports range requests comes in (twice), but the range request for the actual data is never done. Instead I'm getting a bunch of strange request headers as returned by the apache_request_headers() call in the stream function. I'm not getting that on my local dev machine, which also runs Apache.

任何想法都将不胜感激,真的可以把我的头发拉出来.

Any ideas would be greatly appreciated, really pulling my hair out here.

推荐答案

今晚,我花了一段时间解决类似的问题-音频标签在大多数浏览器上都可以正常工作,但在Safari上无法正常处理进度.我找到了一种对我有用的解决方案,希望它对您也有用.

Tonight I spent a while on a similar problem - audio tags that work fine on most browsers, but don't deal with progress properly on Safari. I have found a solution that works for me, hopefully it works for you too.

我还阅读了其他有关类似问题的问题,他们都谈到了如何处理Range标头.有一些摘要旨在解决Range标头.我在github上发现了一个短路函数,该函数对我有用. https://github.com/pomle/php-serveFilePartial

I also read the other SO questions about similar issues, and they all spoke about dealing with the Range header. There are a few snippets floating around that aim to deal with the Range header. I found a short(ish) function on github that has been working for me. https://github.com/pomle/php-serveFilePartial

我确实必须对文件进行一次更改.在第38行:

I did have to make one change to the file though. On line 38:

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

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

I made a small modification (removed a -1)

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

我刚刚在github上发布了一个问题,解释了为什么进行此更改.我发现此问题的方式很有趣:当Safari表示可以提供部分内容时,它似乎不信任服务器:Safari(或技术上我认为是Quicktime的)请求具有这样的范围标头的文件的字节0-1 :

I have posted an issue to the github just now explaining why I made this change. The way I found this issue is interesting: It appears that Safari doesn't trust a server when it says it can provide partial content: Safari (or technically Quicktime I think) requests bytes 0-1 of a file with a range header like this:

Range:  bytes=0-1

作为对文件的第一个请求.如果服务器返回整个文件-它将文件视为流",没有开始或结束.如果服务器使用该文件中的一个字节(和正确的标头)进行响应,则服务器将请求该文件的几个不同范围(以一种非常不合适的方式严重重叠).我发现您已经注意到了这一点,并且您已经体验到Safari/Quicktime仅发出第一个远程(0-1)请求,而没有后续的真实"远程请求.从我的调查中看来,这是由于您的服务器没有提供令人满意的"远程答复而发生的,因此它放弃了整个远程请求的想法.在使用链接的serverFilePartial函数进行调整之前,我遇到了这个问题.但是,在修复"该行之后,Safari/Quicktime似乎对第一个响应感到满意,并继续发出后续的远程请求,并且进度条和所有内容均显示出来,我们都很好.

as its first request to the file. If the server returns the whole file - it treats the file as a 'stream', which has no beginning or end. If the server responds with a single byte from that file, and the correct headers, it will then ask for a few different ranges of that file (which grossly overlap in what seems like a very inappropriate way). I see that you have already noticed this, and that you have experienced that Safari/Quicktime only makes the first ranged (0-1) request, and no subsequent 'real' ranged requests. It appears from my poking-around that this is happening because your server did not serve a 'satisfactory' ranged reply, so it gave up on the whole ranged request idea. I was experiencing this problem when I used the linked serverFilePartial function, before making my adjustment to it. However, after 'fixing' that line, Safari/Quicktime seems to be happy with the first response, and continues to make subsequent ranged requests, and the progress bar and everything appears, and we are all good.

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

So, long story short, give the linked library a go, and see if it works for you like it did for me :) I know you have already found a php solution that works on your dev machine but not on your production machine, but maybe my different solution will be different on your VPS machine? worth a try.

这篇关于Safari无法正确显示php提供的mp3持续时间的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆