Friday, June 4, 2010

PHP: Sending Motion-JPEG

As you may know from past posts, I was trying to send Motion-JPEG from a PHP script. This proved (for many reason) not so easy. After I conquered writing php extension modules, I was still left with nuances in PHP that made it difficult to send MJPEG from my script.

Here's the basic run-down of difficulties:
  • PHP buffers output to the client and this keeps you from doing continuous streams of data easily
  • PHP doesn't allow you to send headers after it thinks the headers have already been sent
  • Apache has some other handlers that also cause buffering
  • Apache does some client negotiation that conflicts with MJPEG (mod_gzip)

Searching the Eentarnets did not produce good results on how to handle this. At least, not in a single place and easily findable. So here's my solution for others to use:

<?
# Used to separate multipart
$boundary = "my_mjpeg";

# We start with the standard headers. PHP allows us this much
header("Cache-Control: no-cache");
header("Cache-Control: private");
header("Pragma: no-cache");
header("Content-type: multipart/x-mixed-replace; boundary=$boundary");

# From here out, we no longer expect to be able to use the header() function
print "--$boundary\n";

# Set this so PHP doesn't timeout during a long stream
set_time_limit(0);

# Disable Apache and PHP's compression of output to the client
@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 0);

# Set implicit flush, and flush all current buffers
@ini_set('implicit_flush', 1);
for ($i = 0; $i < ob_get_level(); $i++)
    ob_end_flush();
ob_implicit_flush(1);

# The loop, producing one jpeg frame per iteration
while (true) {
    # Per-image header, note the two new-lines
    print "Content-type: image/jpeg\n\n";

    # Your function to get one jpeg image
    print get_one_jpeg();

    # The separator
    print "--$boundary\n";
}
?>

That's it in a nutshell. Make sure that your PHP script does not contain any newlines or data before or after the PHP enclosures (<? ... ?>).

7 comments:

  1. "PHP doesn't allow you to send headers after it thinks the headers have already been sent" I can't understand this point. It does not only think, it's really that way! Since the HTTP headers at the beginning of the answer of a HTTP request, so if you output anything, it's simply impossible to send http header, since that protocol state is already gone! So it's not PHP problem, it's the HTTP how it works.

    ReplyDelete
  2. @LGB, that may be true, but in the case of multipart, you can and do send headers after the initial headers are complete. PHP wont allow you to do this with the header() function, so you have to not use that and resort to using print instead.

    The reason being is that header() can replace existing headers in PHP's header state before sending them to the client. In essence, you can prep the headers before sending them.

    So it's not that you can't send headers after the initial ones, you just can't use PHP's header() function because it conflicts with how PHP handles headers internally.

    ReplyDelete
  3. to avoid sending any white space, don't write the php closing tag.

    ReplyDelete
  4. Wow this is great.
    I've been trying to create an artificial stream of JPEGs using javascript but was running into performance problems because I was opening too many TCP connections.

    Never thought of just creating a mjpeg stream.
    Doing this in php just makes it all simpler.
    Thanks

    ReplyDelete
  5. "to avoid sending any white space, don't write the php closing tag." QFT

    ReplyDelete
  6. thank you for this code snippet! used it here: http://hackaday.io/project/1766

    ReplyDelete