PHP: iPad не воспроизводит MP4-видео, доставленные PHP, но при прямом доступе

Я пытаюсь найти решение для этой проблемы уже несколько дней, я попробовал все советы, которые я мог найти здесь, на stackru и других платформах. И все же, нет решения.

Я встраиваю видео через тег HTML5:

 <video poster="thumb.png" controls="controls" preload="none" width="640" height="480">
    <source src="provider.php?secure=12345" type="video/mp4">
 </video>

Я пытаюсь доставить видеофайл MP4 через PHP, а не связывать его напрямую. Связывание файла mp4 напрямую работает и воспроизводит файл!

Тестирование:

  1. видеофайл: https://github.com/q2apro/videotest-ipad/raw/master/video.mp4 (проигрывается на iPad)
  2. видеофайл, загруженный PHP с такими же заголовками: https://github.com/q2apro/videotest-ipad/blob/master/test-headers.php (не воспроизводится на iPad) - исходный код
  3. видеофайл, загруженный PHP с диапазоном байтов: https://github.com/q2apro/videotest-ipad/blob/master/test-byterange.php (не воспроизводится на iPad) - исходный код
  4. видеофайл, загруженный PHP с помощью Byte Ranges (другой скрипт): https://github.com/q2apro/videotest-ipad/blob/master/test-byterange-2.php (не воспроизводится на iPad, предупреждение "Операция может не будет завершено ") - Исходный код

Заметки:

  • все ссылки выше имеют прямой доступ к видеофайлу и без него
  • видео работает во всех браузерах Windows (но не в Safari/Chrome на iPad, возможно, не на iPhone)

Моя настройка:

  • тестирующее устройство: iPad iOS 6 (у меня нет mac, я не могу отлаживать)
  • iPad с Safari и Chrome (пробовал оба браузера)
  • мой сервер является shared-хостингом от domainfactory
  • инструмент для отладки: Консоль для разработчиков Firefox 29 / WIN7

.htaccess в тестовой папке задает MIME-тип и Accept-Ranges:

AddType video/mp4 .mp4 

<IfModule mod_headers.c>
   Header set Accept-Ranges "bytes"
</IfModule>


Несмотря на то, что я создал один и тот же заголовок (сравните тестовые URL 1. и 2.), iPad не воспроизводит файл через запрос PHP.

Вместо этого я всегда получаю эту поразительную кнопку воспроизведения:

ipad поразил кнопку воспроизведения

Заголовки 1. (прямой вызов mp4):

прямой вызов mp4

Заголовки 2. Те же заголовки, что и выше, но настроены PHP (mp4 поставляется PHP):

введите описание изображения здесь

-

Я также попытался прочитать весь видеофайл и отправить его в браузер, используя PHP- функции fread (), fpassthru () и file_get_contents(), но iPad всегда показывает значок "невозможно воспроизвести".

-

Мой размещенный сервер не поддерживает соединения, может ли это быть проблемой? IPad интерпретирует.php отличается от.mp4?

Может ли кто-нибудь помочь мне от боли? Я полностью застрял.



PS: что я пытался рассмотреть:

  • Запросы диапазона байтов (206 частичное содержание) 01 02 03
  • правильное кодирование видео 04
  • использовали другие закодированные видео при тестировании
  • отключено zlib.output_compression в PHP-скриптах



ОБНОВЛЕНИЕ: Консоль отладки

Наконец-то я получил MAC своего друга, подключил iPad, открыл Debug Console в Safari на Mac, загрузил страницу на iPad и проверил сообщения об ошибках, появляющиеся на Mac (кстати, насколько сложнее Apple может заставить нас развиваться)...). Для всех тестовых скриптов эта ошибка появляется:

Failed to load resource: Plug-in handled load

1 ответ

Решение

Вау, это было тяжело!


1. Первая серьезная проблема

Оказалось, что это не проблема кодирования, а проблема с заголовком контейнера mp4, установленным в процессе преобразования видео - у iPad, очевидно, есть проблема с видео MP4, подготовленными для прогрессивной потоковой передачи.

Сначала я обнаружил это в разговоре здесь. После конвертации видео я всегда использовал инструмент MP4 Fast Start для подготовки видеофайла для прогрессивного потока. Это было необходимо для потоковой передачи видеофайла в проигрыватель Flash Player (постепенно), чтобы он не загружал весь файл (и пользователь должен был ждать).

У Handbrake есть похожая настройка, которая называется Web Optimized, Это делает то же самое:

Web Optimized
Also known as "Fast Start"
This places the container header at the start of the file, optimizing it for streaming across the web.

Если вы включите это и преобразуете свое видео, iPad не будет воспроизводить видеофайл! Вместо этого вы получаете ошибку "Операция не может быть завершена".

ipad поразил кнопку воспроизведения

Проверьте и протестируйте сами: видео ресурсы тестирования.


2. Вторая проблема

В производственной среде я всегда использовал PHP для проверки реферера. Как я выяснил, iPad не отправляет информацию о реферере. Это также предотвращает потоковую передачу, и вы также увидите символ невозможности воспроизведения (зачеркнутый значок воспроизведения).


3. Третья проблема

Я не мог выяснить почему, но iPad принимает потоковое видео только из этого скрипта http://ideone.com/NPSlw5

<?php
// disable zlib so that progress bar of player shows up correctly
if(ini_get('zlib.output_compression')) {
    ini_set('zlib.output_compression', 'Off'); 
}

$folder = '.'; 
$filename = 'video.mp4';
$path = $folder.'/'.$filename;

// from: http://licson.net/post/stream-videos-php/
if (file_exists($path)) {
    // Clears the cache and prevent unwanted output
    ob_clean();

    $mime = "video/mp4"; // The MIME type of the file, this should be replaced with your own.
    $size = filesize($path); // The size of the file

    // Send the content type header
    header('Content-type: ' . $mime);

    // Check if it's a HTTP range request
    if(isset($_SERVER['HTTP_RANGE'])){
        // Parse the range header to get the byte offset
        $ranges = array_map(
            'intval', // Parse the parts into integer
            explode(
                '-', // The range separator
                substr($_SERVER['HTTP_RANGE'], 6) // Skip the `bytes=` part of the header
            )
        );

        // If the last range param is empty, it means the EOF (End of File)
        if(!$ranges[1]){
            $ranges[1] = $size - 1;
        }

        // Send the appropriate headers
        header('HTTP/1.1 206 Partial Content');
        header('Accept-Ranges: bytes');
        header('Content-Length: ' . ($ranges[1] - $ranges[0])); // The size of the range

        // Send the ranges we offered
        header(
            sprintf(
                'Content-Range: bytes %d-%d/%d', // The header format
                $ranges[0], // The start range
                $ranges[1], // The end range
                $size // Total size of the file
            )
        );

        // It's time to output the file
        $f = fopen($path, 'rb'); // Open the file in binary mode
        $chunkSize = 8192; // The size of each chunk to output

        // Seek to the requested start range
        fseek($f, $ranges[0]);

        // Start outputting the data
        while(true){
            // Check if we have outputted all the data requested
            if(ftell($f) >= $ranges[1]){
                break;
            }

            // Output the data
            echo fread($f, $chunkSize);

            // Flush the buffer immediately
            @ob_flush();
            flush();
        }
    }
    else {
        // It's not a range request, output the file anyway
        header('Content-Length: ' . $size);

        // Read the file
        @readfile($path);

        // and flush the buffer
        @ob_flush();
        flush();
    }

}
die();

?>


Я надеюсь, что эта информация поможет другим справиться с проблемой.


Обновление: три месяца спустя в производственной среде некоторые из моих пользователей все еще сообщали о проблемах воспроизведения. Кажется, есть еще одна проблема с Safari. Я посоветовал им использовать Chrome для iPad, это исправлено.



PS: пару дней исследований и хлопот только для воспроизведения видеофайла, который, кстати, работает на всех других устройствах. Это еще раз доказывает мне, что Apple добилась успеха только благодаря отличному маркетингу, а не благодаря отличному программному обеспечению.

Спасибо за ваш вклад, очень важно... Но даже ваш код не подошел для моего iphone.

Даже если я до сих пор не знаю почему, следующий код у меня сработал. Вероятно для строки:

header('HTTP/1.1 416 Requested Range Not Satisfiable');

Я получил это отсюда:https://github.com/tikiatua/internal-assets-plugin/issues/9

$fp = fopen($filepath, "rb");
    $size = filesize($filepath);
    $length = $size;
    $start = 0;
    $end = $size - 1;
    header('Content-type: video/mp4');
    header("Accept-Ranges: 0-$length");
    if (isset($_SERVER['HTTP_RANGE'])) {
        $c_start = $start;
        $c_end = $end;
        list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);

        if (strpos($range, ',') !== false) {
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
            header("Content-Range: bytes $start-$end/$size");
            exit;
        }

        if ($range == '-') {
            $c_start = $size - substr($range, 1);
        } else {
            $range = explode('-', $range);
            $c_start = $range[0];
            $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
        }

        $c_end = ($c_end > $end) ? $end : $c_end;

        if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
            header("Content-Range: bytes $start-$end/$size");
            exit;
        }

        $start = $c_start;
        $end = $c_end;
        $length = $end - $start + 1;
        fseek($fp, $start);
        header('HTTP/1.1 206 Partial Content');
    }

    header("Content-Range: bytes $start-$end/$size");
    header("Content-Length: ".$length);

    $buffer = 1024 * 8;

    while(!feof($fp) && ($p = ftell($fp)) <= $end) {
        if ($p + $buffer > $end) {
            $buffer = $end - $p + 1;
        }
        set_time_limit(0);
        echo fread($fp, $buffer);
        flush();
    }

    fclose($fp);
    exit;

(к сожалению, я не могу комментировать, так как я новичок на форуме, так что это новый пост:)

Что касается сценария №3 (Третья проблема): он работал у меня во всех браузерах, включая более старые версии Safari на iOS, но не на текущих версиях Safari, ни на iOS, ни на MacOS.

Уменьшение конца каждого диапазона ($range[1]) на 1 в строке 37:

else {
    $ranges[1]--; 
}

... и расширив Content-Range на 1 (как предложил Конрад) в строке 41:

header('Content-Length: ' . (($ranges[1] - $ranges[0]) + 1));

... работал у меня и решил проблему для текущих версий Safari, хотя более старые версии (например, на iOS 9.3.5) теперь, похоже, больше не работают.

Другие вопросы по тегам