multipart / x-mixed-replace ActionScript3和谷歌Chrome(以及其他)

时间:2011-02-26 20:43:44

标签: flash actionscript-3 google-chrome twisted xmlhttprequest

我有一个奇怪的问题,我正在开发蓝牙相机,我们希望为世界提供一个mjpeg接口。

Mjpeg只是一个http服务器,在连接保持打开的情况下回复一个jpeg。我的服务器现在正在给我:

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

然后为每一帧:

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

BINARY JPEG CONTENT.....
(new line)

我为它制作了一个Flash客户端,因此我们可以在任何设备上使用相同的代码,服务器使用twisted实现Python,并且针对Android等等,Android中的问题是Google 忘记了包括mjpeg支持....这个客户端正在使用URLStream。

代码是这样的:

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);
    }

  }
};

客户端在Firefox上工作得很好,我得到了所有的http标题:

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

所以我使用content-size来知道要前进多少字节。在IE8中也是如此(即使有错误的IE兼容!)

在Safari上它的工作方式有点不同(也许是webkit这样做)我没有得到http片段只是二进制内容,这迫使我在缓冲区中搜索帧的开始和结束。

问题是Chrome,无论信不信,它都无法正常工作。有些奇怪的事情发生了,显然我得到了第一个tcp / ip包然后由于某种原因Chrome决定关闭连接,日志的输出是这样的:

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

我不应该在服务器关闭连接之前获取httpStatus,而不是这里的情况。

请不要告诉我使用HTML5 Canvas或Video我都是这样做的,问题是我们希望这个应用程序在许多操作系统中运行并为所有它们编译视频编码器(例如ffmpeg)将不会工作更轻松。另外我们想提供SCO音频,它只是一个PCM流,所以我不能使用普通的mjpeg。 Canvas太慢了,我测试过,特别是在Android上。

2 个答案:

答案 0 :(得分:3)

最后我发现了问题!

根据Chrome的Flash插件,内容类型有误,正确的是: Content-Type: multipart/x-mixed-replace

而不是 Content-Type: multipart/x-mixed-replace; boundary=myBOUNDARY

所以我的服务器现在根据请求参数发送或不发送边界。

答案 1 :(得分:1)

这对我来说也不起作用 - 最终我使用chrome进行了工作:

Content-Type:text / html; boundary = - myboundary

我生命中有6个小时:(