Оригинальный вопрос Я обслуживаю mp3 файл с помощью действия контроллера ZF2. Это прекрасно работает во всех браузерах, кроме Safari на OS X и iPhone/iPad. Звук воспроизводится, но продолжительность отображается только как 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:
Я также попытался интерпретировать заголовок запроса 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(), но это тоже не работает.
Любые идеи о том, как решить эту проблему, приветствуются.
Edit Как было предложено @MarcB, я сравнивал заголовки ответов двух запросов. Первый запрос - это действие PHP, служащее файлами данных mp3, а второе - когда я просматриваю один и тот же mp3 файл напрямую. Сначала заголовки были не совсем одинаковыми, но я изменил PHP script, чтобы соответствовать заголовкам прямой загрузки, см. Ниже скриншоты Firebug:
-
Заголовки ответов, обслуживаемые PHP:
-
Заголовок заголовка ответов:
Как вы можете видеть, они точно такие же, за исключением заголовка Date, но это потому, что между запросами было около полутора минут. Тем не менее Safari утверждает, что это прямая трансляция, когда я пытаюсь обслуживать файл с PHP script, и поэтому аудиоплеер все еще показывает NaN в течение общего времени, когда я загружаю его таким образом. Есть ли способ сказать Safari просто загрузить весь файл и просто доверять мне, когда я говорю, что это не прямая трансляция?
Также может быть, что Safari отправляет разные заголовки запросов, и, следовательно, заголовки ответов также различны? Я обычно делаю свою отладку в Firefox с помощью Firebug. Когда я открываю URL-адрес файла MP3 в Safari, например, я не могу открыть диалоговое окно Web Inspector. Есть ли другой способ просмотреть, какие заголовки отправляются и принимаются Safari?
Изменить 2 Теперь я использую простую функцию потока, реализующую запросы диапазона. Это, похоже, работает на моей машине dev даже в 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);
Я добавил несколько протоколов для целей отладки, и разница, похоже, находится в заголовках запросов. На моей локальной машине dev, где она работает, я получаю это в журнале:
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() в функции потока. Я не получаю это на своей локальной машине dev, которая также запускает Apache.
Любые идеи были бы очень признательны, действительно вытащили бы мои волосы здесь.