I am trying to find a solution for this problem for some days already, I tried all advices I could find here on stackoverflow and other platforms. And still, there is no solution.
I am embedding a video via HTML5 video tag:
<video poster="thumb.png" controls="controls" preload="none" width="640" height="480">
<source src="provider.php?secure=12345" type="video/mp4">
</video>
I try to deliver the MP4 video file by PHP instead of linking it directly. Linking the mp4 file directly works and plays the file!
Testing:
Notes:
My Setup:
The .htaccess
in the test folder sets the MIME type and Accept-Ranges:
AddType video/mp4 .mp4
<IfModule mod_headers.c>
Header set Accept-Ranges "bytes"
</IfModule>
Even though I created the same header (compare test URLs 1. and 2.), the iPad is not playing the file through the PHP request.
Instead I always get this strikedthrough play button:
The headers of 1. (direct mp4 call):
The headers of 2. The same headers as above, but set up by PHP (mp4 delivered by PHP):
--
I have also tried reading the entire video file and sending it to the browser using PHP's fread(), fpassthru() and file_get_contents() but the iPad is always showing the cannot-play-icon.
--
My hosted server does not supply Connection keep-alive, could this be a problem? Is the iPad interpreting .php different from .mp4?
Can somebody help me out of pain? I am totally stuck.
PS: What I tried to consider:
I finally got a MAC of a friend, connected the iPad, opened the Debug Console in Safari on the Mac, loaded the page on the iPad and checked the error messages appearing on the Mac (btw, how more complicated could apple force us to develop...). For all test scripts this error appears:
Failed to load resource: Plug-in handled load
Wow, that was tough!
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".
Check out and test it yourself: video test resources.
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).
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.'/'.$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.
Thanks for your contribution, very important... But even your code didn't make it for my iphone.
Even if I still don't know why, the following code worked for me. Probably for the line:
header('HTTP/1.1 416 Requested Range Not Satisfiable');
I got this from here: 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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With