multipart/x-mixed-replace ActionScript3 and Google Chrome (and others as well)
Question: I have a strange problem, I'm working on a Bluetooth camera we want to provide
an mjpeg interface to the world.

Mjpeg is just an http server replying one jpeg after the other with the
connection keept open. My server is right now giving me:

    
    
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    Cache-Directive: no-cache
    Expires: 0
    Pragma-Directive: no-cache
    Server: TwistedWeb/10.0.0
    Connection: Keep-Alive
    Pragma: no-cache
    Cache-Control: no-cache, no-store, must-revalidate;
    Date: Sat, 26 Feb 2011 20:29:56 GMT
    Content-Type: multipart/x-mixed-replace; boundary=myBOUNDARY
    
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    Cache-Directive: no-cache
    Expires: 0
    Pragma-Directive: no-cache
    Server: TwistedWeb/10.0.0
    Connection: Keep-Alive
    Pragma: no-cache
    Cache-Control: no-cache, no-store, must-revalidate;
    Cate: Sat, 26 Feb 2011 20:29:56 GMT
    Content-Type: multipart/x-mixed-replace; boundary=myBOUNDARY
    

And then for each frame:

    
    
    --myBOUNDARY
    Content-Type: image/jpeg
    Content-Size: 25992
    
    BINARY JPEG CONTENT.....
    (new line)
    

I made a Flash client for it, so we can use the same code on any device, the
server is implemented in Python using twisted and is targeting Android among
others, problem in Android is Google **forgot** to include mjpeg support....
This client is using URLStream.

The code is this:

    
    
    package net.aircable {
      import flash.errors.*;
      import flash.events.*;
      import flash.net.URLRequest;
      import flash.net.URLRequestMethod;
      import flash.net.URLRequestHeader;
      import flash.net.URLStream;
      import flash.utils.ByteArray;
      import flash.utils.Dictionary;
      import flash.system.Security;
      import mx.utils.Base64Encoder;
      import flash.external.ExternalInterface;
      import net.aircable.XHRMultipartEvent;
    
      public class XHRMultipart extends EventDispatcher{
    
        private function trc(what: String): void{
            //ExternalInterface.call("console.log", what); //for android
            trace(what);
        }
    
        private var uri: String;
        private var username: String;
        private var password: String;
        private var stream: URLStream;
        private var buffer: ByteArray;
        private var pending: int;
        private var flag: Boolean;
        private var type: String;
        private var browser: String;
    
        private function connect(): void {
          stream = new URLStream();
          trc("connect")
          var request:URLRequest = new URLRequest(uri);
          request.method = URLRequestMethod.POST;
          request.contentType = "multipart/x-mixed-replace";
          trc(request.contentType)
    /*      request.requestHeaders = new Array(
            new URLRequestHeader("Content-type", "multipart/x-mixed-replace"),
            new URLRequestHeader("connection", "keep-alive"),
            new URLRequestHeader("keep-alive", "115"));
    */
          trace(request.requestHeaders);
          trc("request.requestHeaders")
          configureListeners();
          try {
            trc("connecting");
            stream.load(request);
            trc("connected")
          } catch (error:Error){
              trc("Unable to load requested resource");
          }
          this.pending = 0;
          this.flag = false;
          this.buffer = new ByteArray();
        }
    
        public function XHRMultipart(uri: String = null, 
                                            username: String = null, 
                                            password: String = null){
          trc("XHRMultipart()");
          var v : String = ExternalInterface.call("function(){return navigator.appVersion+'-'+navigator.appName;}");
          trc(v);
          v=v.toLowerCase();
          if (v.indexOf("chrome") > -1){
            browser="chrome";
          } else if (v.indexOf("safari") > -1){
            browser="safari";
          }
          else {
            browser=null;
          }
          trc(browser);
          if (uri == null)
            uri = "../stream?ohhworldIhatethecrap.mjpeg";
          this.uri = uri;
          connect();
        }
    
    
        private function configureListeners(): void{
          stream.addEventListener(Event.COMPLETE, completeHandler, false, 0, true);
          stream.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler, false, 0, true);
          stream.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler, false, 0, true);
          stream.addEventListener(Event.OPEN, openHandler, false, 0, true);
          stream.addEventListener(ProgressEvent.PROGRESS, progressHandler, false, 0, true);
          stream.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler, false, 0, true);
        }
    
        private function propagatePart(out: ByteArray, type: String): void{
            trc("found " + out.length + " mime: " + type);
            dispatchEvent(new XHRMultipartEvent(XHRMultipartEvent.GOT_DATA, true, false, out));
        }
    
        private function readLine(): String {
            var out: String = "";
            var temp: String;
    
            while (true){
                if (stream.bytesAvailable == 0)
                    break;
                temp = stream.readUTFBytes(1);
                if (temp == "\n")
                    break;
                out+=temp;
            }
            return out;
        }
    
        private function extractHeader(): void {
            var line: String;
            var headers: Object = {};
            var head: Array;
    
            while ( (line=readLine()) != "" ){
                if ( stream.bytesAvailable == 0)
                    return;
                if (line.indexOf('--') > -1){
                    continue;
                }
                head = line.split(":");
                if (head.length==2){
                    headers[head[0].toLowerCase()]=head[1];
                }
            }
    
            pending=int(headers["content-size"]);
            type = headers["content-type"];
            if ( pending > 0 && type != null)
                flag = true;
            trc("pending: " + pending + " type: " + type);
        }
    
        private function firefoxExtract(): void {
            trc("firefoxPrepareToExtract");
            if (stream.bytesAvailable == 0){
                trc("No more bytes, aborting")
                return;
            }
    
            while ( flag == false ) {
                if (stream.bytesAvailable == 0){
                    trc("No more bytes, aborting - can't extract headers");
                    return;
                }
                extractHeader()
            }
    
            trc("so far have: " + stream.bytesAvailable);
            trc("we need: " + pending);
            if (stream.bytesAvailable =0; x-=1){
                buffer.position=x;
                buffer.readBytes(temp, 0, 2);
                // check if we found end marker
                if (temp[0]==0xff && temp[1]==0xd9){
                    end=x;
                    break;
                }
            }
    
            trc("findImageInBuffer, start: " + start + " end: " + end);
            if (start >-1 && end > -1){
                var output: ByteArray = new ByteArray();
                buffer.position=start;
                buffer.readBytes(output, 0 , end-start);
                propagatePart(output, type);
                buffer.position=0; // drop everything
                buffer.length=0;
            }
        }
    
        private function safariExtract(): void {
            trc("safariExtract()");
            stream.readBytes(buffer, buffer.length);
            findImageInBuffer();
        }
    
        private function chromeExtract(): void {
            trc("chromeExtract()");
            stream.readBytes(buffer, buffer.length);
            findImageInBuffer();
        }
    
        private function extractImage(): void {
            trc("extractImage");
    
            if (browser == null){
                firefoxExtract();
            }
            else if (browser == "safari"){
                safariExtract();
            }
            else if (browser == "chrome"){
                chromeExtract();
            }
        }
    
        private function isCompressed():Boolean {
            return (stream.readUTFBytes(3) == ZLIB_CODE);
        }
    
        private function completeHandler(event:Event):void {
            trc("completeHandler: " + event);
            //extractImage();
            //connect();
        }
    
        private function openHandler(event:Event):void {
            trc("openHandler: " + event);
        }
    
        private function progressHandler(event:ProgressEvent):void {
            trc("progressHandler: " + event)
            trc("available: " + stream.bytesAvailable);
            extractImage();
            if (event.type == ProgressEvent.PROGRESS)
                if (event.bytesLoaded > 1048576) { //1*1024*1024 bytes = 1MB
                    trc("transfered " + event.bytesLoaded +" closing")
                    stream.close();
                    connect();
                }
        }
    
        private function securityErrorHandler(event:SecurityErrorEvent):void {
            trc("securityErrorHandler: " + event);
        }
    
        private function httpStatusHandler(event:HTTPStatusEvent):void {
            trc("httpStatusHandler: " + event);
            trc("available: " + stream.bytesAvailable);
            extractImage();
            //connect();
        }
    
        private function ioErrorHandler(event:IOErrorEvent):void {
            trc("ioErrorHandler: " + event);
        }
    
      }
    };
    

The client is working quite well on Firefox where I get all the http header:

    
    
    --myBOUNDARY
    Content-Type: image/jpeg
    Content-Size: 25992
    

So I use content-size to know how many bytes to go ahead. Same happens in IE8
(even buggy IE is compatible!)

On Safari it works a bit differently (maybe it's webkit doing it) I don't get
the http piece just the Binary content, which forces me to search over the
buffer for the start and end of frame.

Problem is Chrome, believe or not, it's not working. Something weird is going
on, apparently I get the first tcp/ip package and then for some reason Chrome
decides to close the connection, the output of the log is this:

    
    
    XHRMultipart()
    5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.114 Safari/534.16-Netscape
    chrome
    connect
    multipart/x-mixed-replace
    
    request.requestHeaders
    connecting
    connected
    openHandler: [Event type="open" bubbles=false cancelable=false eventPhase=2]
    openHandler: [Event type="open" bubbles=false cancelable=false eventPhase=2]
    progressHandler: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=3680 bytesTotal=0]
    available: 3680
    extractImage
    chromeExtract()
    findImageInBuffer, start: 0 end: -1
    httpStatusHandler: [HTTPStatusEvent type="httpStatus" bubbles=false cancelable=false eventPhase=2 status=200 responseURL=null]
    available: 0
    extractImage
    chromeExtract()
    findImageInBuffer, start: 0 end: -1
    

I shouldn't be getting httpStatus until the server closes the connection which
is not the case here.

Please don't tell me to use HTML5 Canvas or Video I all ready been that way,
problem is we want this application to run in many OSes and compiling a video
encoder for all them (ffmpeg for example) will not make the work any easier.
Also we want to provide with SCO audio which is just a PCM stream, so I can't
use plain mjpeg. Canvas is too slow, I tested that, specially on Android.


Answer: Finally I found the problem!

Content-type is wrong according to Chrome's flash plugin, the correct one is:
`Content-Type: multipart/x-mixed-replace`

And not `Content-Type: multipart/x-mixed-replace; boundary=myBOUNDARY`

So my server now sends or not the boundary depending on a request argument.

