PHP: iPad does not play MP4 videos delivered by PHP, but if accessed directly it does

Wow, that was tough!

**1. First major Problem**

It turned out to be no encoding problem but a problem with the mp4 container header set during the video conversion process – iPad has obviously a problem with MP4 videos that are prepared for progressive streaming.

First I discovered that in a conversation here. After converting a video I always used the tool MP4 Fast Start to prepare the video file for progressive stream. This was necessary to stream the video file to the Flash Player in pieces (progressively), so it did not load the entire file (and the user had to wait).

With Handbrake there is a similar setting, that is called Web Optimized. It does the same:

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.

If you enable this and convert your video, the iPad will not play the video file! Instead you get the error “The operation could not be completed”.

ipad strikedthrough play button

Check out and test it yourself: video test resources.

**2. Second Problem**

In production environment I always used PHP to check the referer. As I found out, the iPad does not send the referer information. This also prevents the streaming and you will also see the cannot-play-symbol (striked-through play icon).

**3. Third Problem**

I could not find out why, but the iPad only accepts the video streaming from this script 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."https://stackoverflow.com/".$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();

?>

I hope this information will help others to cope with the problem.

Update: Three months later in production environment, some of my users still reported playback issues. There seems to be another problem with Safari. I advised them to use Chrome for iPad, this fixed it.

PS: A couple of days of research and hassle only to play a video file that, by the way, runs on all other devices. This again proves to me that Apple got successful just because of great marketing, not because of great software.

Leave a Comment