tags:

views:

123

answers:

3

Bizarre bug: in a PHP script that serves video files, I have a few test conditions (authenticate token, make sure file exists, etc.) before sending a "video/mp4" header and outputting an MP4 file.

If any of the tests fail, $fail is given a non-false value.

At the end of the tests there's this if statement:

if ($fail) {
    exit;
}

This code works as expected in Chrome, but not in Safari. However (and believe me, I've tested this every which way), if I simply comment out exit;, as in:

if ($fail) {
    //exit;
}

... the code works perfectly in Safari — the video immediately starts loading.

I'm sure that that if block is never entered, otherwise the script would stop executing, and I wouldn't be seeing the video/mp4 header (not to mention, it wouldn't work in Chrome). Further, whatever PHP is doing behind the scenes should be totally transparent to the browser. I thought maybe there was some issue with output, but I would have received a warning if I had outputted anything before the headers.

I've been seeing this behavior consistently for days — I've checked it probably 25 times in disbelief.

Surely there's something I'm missing?

UPDATE

To clarify the problem, I changed the code a bit:

$fail = true;
if ($fail) {
    die('Fail');
}

Now we're guaranteed to hit the die() statement, and output "Fail". Here are the headers, as seen by Safari:

Connection:Keep-Alive
Content-Type:text/html
Date:Thu, 24 Jun 2010 23:31:28 GMT
Keep-Alive:timeout=10, max=29
Server:Apache/2.2.15 (CentOS) mod_ssl/2.2.15 0.9.8l DAV/2 mod_auth_passthrough/2.1 FrontPage/5.0.2.2635
Transfer-Encoding:Identity
X-Powered-By:PHP/5.2.13

("Fail" is outputted as expected, too.)

Now, when I comment-out $fail = true;, the headers change to:

Connection:Keep-Alive
Content-Length:47406944
Content-Type:video/mp4
Date:Thu, 24 Jun 2010 23:32:58 GMT
Keep-Alive:timeout=10, max=30
Server:Apache/2.2.15 (CentOS) mod_ssl/2.2.15 0.9.8l DAV/2 mod_auth_passthrough/2.1 FrontPage/5.0.2.2635
X-Powered-By:PHP/5.2.13

But the video still won't play! (QuickTime logo with a question mark over it.)

I think that's evidence enough that $fail remains false, and that die() is never executed.

Now, get this: if I again comment out die() (functionally equivalent to exit), so that my final code is:

//$fail = true;
if ($fail) {
    //die('Fail');
}

... the video plays in Safari!

UPDATE 2

If I change the code to:

$fail = false;
if ($fail) {
    die('Fail');
}

... to absolutely guarantee that $fail is false, it plays in Safari!

This behavior makes no sense to me, b/c if $fail was being set due to one of my verification conditions, then it would never output the video/mp4 header, as when I explicitly set $fail to true — and instead it would output a text/html page with the word "Fail" — right?

UPDATE 3

Here's all of the relevant code, just to be totally clear:

// verify
$fail = false;
$token = false;
$file_name = [rest assured that $file_name is correct];
if (!$file_name) {
    $fail = true;
} else {
    $file_name = '../video/'.$file_name;
}
if (!isset($_REQUEST['ts'])) {
    $fail = true;
}
if (isset($_POST['token']) || isset($_GET['token'])) {
    $token = isset($_POST['token']) ? $_POST['token'] : $_GET['token'];
} else if (isset($_COOKIE['token'])) {
    $token = $_COOKIE['token'];
}
if ($token != md5(SALT_HASH.$_REQUEST['ts'])) {
    $fail = true;
}
if (((int)($_REQUEST['ts']) + 60 * 10) < mktime()) {
    $fail = true;
}
if (!is_file($file_name)) {
    $fail = true;
}
if ($fail) {
    die('Fail');
}

// output
$file_size = (string)(filesize($file_name));
header('Content-Type: video/mp4');
header('Content-Length: '.$file_size);
readfile_chunked($file_name);
exit;

HOPEFULLY THE LAST UPDATE / SUMMARY

This question is probably already too long, but I thought I'd try one last time to summarize how weird this is. There are 3 distinct responses:

1) If I hardwire a $fail = true; before if ($fail) die('Fail');, just so I have a baseline for failure, I get a text/html header, and the word "Fail" is outputted as expected.

2) If I leave the code as it is above, I get a video/mp4 header, but a broken video in Safari (it will play in Chrome).

3) Finally (and this is based on new testing I did today), if I comment out $fail = true; in the token-verification conditional, I get a video/mp4 header, and the video plays in Safari. Now, I figured there must something wrong with the token verification — but when I hardwire in another test to echo the value of $fail after the test, it's still false! The conditional is never entered (I also just put a straight die('!'); instead of $fail = true; — and I still get a video/mp4 header).

I simply cannot believe that commenting out code that never gets executed could cause a distinct response — and furthermore, that it would play in Chrome, but not in Safari; whatever's happening server-side with PHP should be completely transparent to the browser.

Crazy.

AHA!

I added some logging to my script, and it turns out that when serving HTML5 video, the browser makes two requests.

Here's the two (successful) requests from Chrome:

Fri Jun 25 17:41:22 2010 Browser: [Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4] Fail: [0] Token: [83e50b519c0ed4662b6b7fabb8f6671e] Timestamp: [1277509282]
Fri Jun 25 17:41:22 2010 Verification passed

Fri Jun 25 17:41:22 2010 Browser: [Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4] Fail: [0] Token: [83e50b519c0ed4662b6b7fabb8f6671e] Timestamp: [1277509282]
Fri Jun 25 17:41:22 2010 Verification passed

And here's the two from Safari (first successful, 2nd fails):

Fri Jun 25 17:41:32 2010 Browser: [Apple Mac OS X v10.6.4 CoreMedia v1.0.0.10F569] Fail: [0] Token: [6374fba3d9eac7d94de9741db76953c6] Timestamp: [1277509291]
Fri Jun 25 17:41:32 2010 Verification passed

Fri Jun 25 17:41:33 2010 Browser: [QuickTime/7.6.6 (qtver=7.6.6;cpu=IA32;os=Mac 10.6.4)] Fail: [1] Token: [] Timestamp: [1277509291]

I have to go out right now, but I'm almost certain this behavior is the root of the bug. For some reason, the 2nd Safari request can't find the token.

A: 

The only reason for it to render a blank page would be that it does in fact enter the exit; statement. You mentioned an authentication token, could that be missing when testing in safari? Firebug and Safari's developer tools can also help you with debugging (check the response headers, etc).

igorw
It *does* show a blank page when I add `$fail = true;` before my `die()` conditional (see clarification in original question). There are 3 distinct responses: text/html, video/mp4 (video plays), and video/mp4 (video broken). So weird.
JKS
A: 

Without the video/mp4 header, the server will send the default content type header, usually text/html, some browser may look at what's being served and ignore the MIME type.

Add error_reporting(E_ALL);to the beginning of the script, and use firebug or similar to examine the headers being sent.

Pete
Right — that's exactly what it does: without the `video/mp4` header, it reverts to `text/html`. But then there's this weird case in Safari where it *does* send the `video/mp4` header, but doesn't play the video (unless I comment out `exit/die`). Banging my head on the table.
JKS
Are you defining `$fail=FALSE` at the very beginning, or are you trying to evaluate an (potentially) undefined variable?
Pete
Yes, there's a `$fail = false;` at the beginning of the script, even when I add it a 2nd time before the `if` statement.
JKS
+1  A: 

It turns out that browsers make two requests to a <video> source before playing it. I was only seeing the headers for the first requests, hence my confusion. The reason the video played in Chrome but not Safari is because the agent that makes the 2nd request in Safari ("QuickTime/7.6.6 (qtver=7.6.6;cpu=IA32;os=Mac 10.6.4)") apparently can't read the same $_COOKIEs; whereas with Chrome, the $_COOKIEs are intact across both requests.

I still have a serious problem, in that the video plays only on desktop browsers, and not on the iPhone. I'm sure it's not an encoding problem, because if I access the MP4 directly, it plays fine; the problem occurs only when the video is outputted through PHP. Anyway, that's another question altogether.

JKS
Useful to know, well done for finding the problem
Pete