【发布时间】:2014-04-08 10:07:06
【问题描述】:
原始问题 我正在通过 ZF2 控制器操作提供 mp3 文件。除了 OS X 和 iPhone/iPad 上的 Safari 之外,这在所有浏览器中都可以正常工作。 音频播放,但持续时间仅显示为 NaN:NaN,而在所有其他浏览器中显示正确的持续时间。
我查看了 SO 上所有讨论相同问题的线程,它似乎与响应标头以及 Content-Range 和 Accept-Ranges 标头有关。我尝试了所有不同的组合,但仍然无济于事 - Safari 仍然拒绝正确显示持续时间。
相关代码sn-p如下所示:
$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 中看起来像这样:
我还尝试根据另一个示例解释 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 对象来呈现响应,使用 header() 调用和 readfile(),但这也不起作用。
欢迎任何关于如何解决此问题的想法。
编辑 正如@MarcB 所建议的,我比较了两个请求的响应标头。第一个请求是针对提供 mp3 文件数据的 PHP 操作,第二个请求是当我直接浏览到同一个 mp3 文件时。一开始头文件并不完全相同,但我修改了PHP脚本以匹配直接下载的头文件,请参见下面的Firebug截图:
PHP 提供的响应标头:
响应头直接下载:
正如您所见,除了 Date 标头之外,它们完全相同,但这是因为请求之间存在大约一分半钟。当我尝试从 PHP 脚本提供文件时,Safari 仍然声称它是现场直播,因此当我以这种方式加载它时,音频播放器仍然显示 NaN。当我说这不是直播时,有什么方法可以告诉 Safari 下载整个文件并相信我?
也可能是 Safari 发送不同的请求标头,因此响应标头也不同?我通常使用 Firebug 在 Firefox 中进行调试。例如,当我在 Safari 中打开 mp3 文件 URL 时,我无法打开 Web Inspector 对话框。有没有其他方法可以查看 Safari 发送和接收的标头?
编辑 2 我现在使用一个简单的流函数来实现范围请求。即使在 Safari 中,这似乎也适用于我的开发机器,但不适用于运行该站点的实时 VPS 服务器。
我现在使用的函数(由另一个 SO-er 提供,不记得确切的链接):
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 使用它来确定服务器是否支持范围请求(两次),但对实际数据的范围请求从未完成。相反,我得到了一堆奇怪的请求标头,这些标头由流函数中的 apache_request_headers() 调用返回。我没有在我的本地开发机器上得到它,它也运行 Apache。
任何想法都将不胜感激,真的把我的头发拉出来了。
【问题讨论】:
-
mp3 的播放时间只有在整个文件被下载并解析以计算其中有多少 mp3 帧后才能确定。一些玩家可以根据观看多个帧并查看帧的比特率是变化还是保持不变来猜测它可能是 cbr。
-
@MarcB 很公平,但是当我将它指向直接提供的文件(而不是来自 PHP 文件)时,为什么它会起作用。在这种情况下,Safari 会立即显示总时间。
-
其实不知道。我会尝试比较两个请求/响应之间的标头,看看有什么不同。
-
您好。您的问题目前以相反的顺序阅读 - 通常最好保持问题原样,并将内容添加到末尾。
-
@halfer 你说得对,我更新了订单。
标签: php zend-framework2 mp3