Sisyphus repositório
Última atualização: 1 outubro 2023 | SRPMs: 18631 | Visitas: 37737436
en ru br
ALT Linux repositórios
S:0.3.0-alt1

Group :: Rede/Acesso Remoto
RPM: spice-html5

 Main   Changelog   Spec   Patches   Sources   Download   Gear   Bugs e FR  Repocop 

Patch: spice-html5-gitf9f700e.patch
Download


 README              |   4 +-
 TODO                |   2 +-
 bitmap.js           |  20 +++++----
 cursor.js           |   2 +-
 display.js          | 119 +++++++++++++++++++++++++++++++++-------------------
 enums.js            |  11 ++++-
 lz.js               |  16 +++++++
 main.js             |  13 +++++-
 playback.js         | 106 +++++++++++++++++++++++++++++++---------------
 port.js             |  85 +++++++++++++++++++++++++++++++++++++
 resize.js           |  20 +++++++--
 simulatecursor.js   |   6 +--
 spice.css           |   1 -
 spice.html          |  71 ++++++++++++++++++++++---------
 spice_auto.html     |  15 ++++++-
 spiceconn.js        |  39 ++++++++++-------
 spicedataview.js    |   8 ++--
 spicemsg.js         |  24 +++++++++--
 spicetype.js        |   2 +-
 thirdparty/jsbn.js  |   6 +--
 thirdparty/prng4.js |   6 +--
 thirdparty/rng.js   |   8 ++--
 thirdparty/rsa.js   |   6 +--
 utils.js            |  12 ++++++
 webm.js             |   1 +
 wire.js             |   2 +-
 26 files changed, 449 insertions(+), 156 deletions(-)
diff --git a/README b/README
index 89d3747..6443000 100644
--- a/README
+++ b/README
@@ -5,7 +5,7 @@ Instructions and status as of August, 2016.
 Requirements:
 
   1.  Modern Firefox or Chrome (IE will work, but badly)
-      
+
   2.  A WebSocket proxy
 
       websockify:
@@ -24,7 +24,7 @@ Optional:
 
       With firefox, you can just open file:///your-path-to-spice.html-here
 
-      With Chrome, you have to set a secret config flag to do that, or 
+      With Chrome, you have to set a secret config flag to do that, or
       serve the files from a web server.
 
 
diff --git a/TODO b/TODO
index 4d4b115..64fc326 100644
--- a/TODO
+++ b/TODO
@@ -6,7 +6,7 @@ Medium Tasks:
     *only* about messages)
 
   . Change the message processing to be able to handle
-    an array of ArrayBuffers so we don't have to 
+    an array of ArrayBuffers so we don't have to
     use the combine function, which is presumed slow.
 
   . Use the 'real' DataView if we have it
diff --git a/bitmap.js b/bitmap.js
index 03f5127..91278d7 100644
--- a/bitmap.js
+++ b/bitmap.js
@@ -26,25 +26,31 @@
 function convert_spice_bitmap_to_web(context, spice_bitmap)
 {
     var ret;
-    var offset, x;
+    var offset, x, src_offset = 0, src_dec = 0;
     var u8 = new Uint8Array(spice_bitmap.data);
     if (spice_bitmap.format != SPICE_BITMAP_FMT_32BIT &&
         spice_bitmap.format != SPICE_BITMAP_FMT_RGBA)
         return undefined;
 
+    if (!(spice_bitmap.flags & SPICE_BITMAP_FLAGS_TOP_DOWN))
+    {
+        src_offset = (spice_bitmap.y - 1 ) * spice_bitmap.stride;
+        src_dec = 2 * spice_bitmap.stride;
+    }
+
     ret = context.createImageData(spice_bitmap.x, spice_bitmap.y);
-    for (offset = 0; offset < (spice_bitmap.y * spice_bitmap.stride); )
-        for (x = 0; x < spice_bitmap.x; x++, offset += 4)
+    for (offset = 0; offset < (spice_bitmap.y * spice_bitmap.stride); src_offset -= src_dec)
+        for (x = 0; x < spice_bitmap.x; x++, offset += 4, src_offset += 4)
         {
-            ret.data[offset + 0 ] = u8[offset + 2];
-            ret.data[offset + 1 ] = u8[offset + 1];
-            ret.data[offset + 2 ] = u8[offset + 0];
+            ret.data[offset + 0 ] = u8[src_offset + 2];
+            ret.data[offset + 1 ] = u8[src_offset + 1];
+            ret.data[offset + 2 ] = u8[src_offset + 0];
 
             // FIXME - We effectively treat all images as having SPICE_IMAGE_FLAGS_HIGH_BITS_SET
             if (spice_bitmap.format == SPICE_BITMAP_FMT_32BIT)
                 ret.data[offset + 3] = 255;
             else
-                ret.data[offset + 3] = u8[offset];
+                ret.data[offset + 3] = u8[src_offset];
         }
 
     return ret;
diff --git a/cursor.js b/cursor.js
index 296fbde..d3f4d55 100644
--- a/cursor.js
+++ b/cursor.js
@@ -118,7 +118,7 @@ SpiceCursorConn.prototype.process_channel_message = function(msg)
 SpiceCursorConn.prototype.set_cursor = function(cursor)
 {
     var pngstr = create_rgba_png(cursor.header.height, cursor.header.width, cursor.data);
-    var curstr = 'url(data:image/png,' + pngstr + ') ' + 
+    var curstr = 'url(data:image/png,' + pngstr + ') ' +
         cursor.header.hot_spot_x + ' ' + cursor.header.hot_spot_y + ", default";
     var screen = document.getElementById(this.parent.screen_id);
     screen.style.cursor = 'auto';
diff --git a/display.js b/display.js
index 12fbab0..f6c74f5 100644
--- a/display.js
+++ b/display.js
@@ -142,7 +142,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                     { base: draw_copy.base,
                       src_area: draw_copy.data.src_area,
                       image_data: this.cache[draw_copy.data.src_bitmap.descriptor.id],
-                      tag: "copycache." + draw_copy.data.src_bitmap.descriptor.id, 
+                      tag: "copycache." + draw_copy.data.src_bitmap.descriptor.id,
                       has_alpha: true, /* FIXME - may want this to be false... */
                       descriptor : draw_copy.data.src_bitmap.descriptor
                     });
@@ -200,7 +200,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                     tmpstr += qdv[i].toString(16);
                 }
 
-                img.o = 
+                img.o =
                     { base: draw_copy.base,
                       tag: "jpeg." + draw_copy.data.src_bitmap.surface_id,
                       descriptor : draw_copy.data.src_bitmap.descriptor,
@@ -233,7 +233,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                     tmpstr += qdv[i].toString(16);
                 }
 
-                img.o = 
+                img.o =
                     { base: draw_copy.base,
                       tag: "jpeg." + draw_copy.data.src_bitmap.surface_id,
                       descriptor : draw_copy.data.src_bitmap.descriptor,
@@ -265,7 +265,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                                         draw_copy.data.src_bitmap.bitmap);
                 if (! source_img)
                 {
-                    this.log_warn("FIXME: Unable to interpret bitmap of format: " + 
+                    this.log_warn("FIXME: Unable to interpret bitmap of format: " +
                         draw_copy.data.src_bitmap.bitmap.format);
                     return false;
                 }
@@ -288,14 +288,11 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                     return false;
                 }
 
-                if (draw_copy.data.src_bitmap.lz_rgb.top_down != 1)
-                    this.log_warn("FIXME: Implement non top down support for lz_rgb");
-
                 var source_img = convert_spice_lz_to_web(canvas.context,
                                             draw_copy.data.src_bitmap.lz_rgb);
                 if (! source_img)
                 {
-                    this.log_warn("FIXME: Unable to interpret bitmap of type: " + 
+                    this.log_warn("FIXME: Unable to interpret bitmap of type: " +
                         draw_copy.data.src_bitmap.lz_rgb.type);
                     return false;
                 }
@@ -359,7 +356,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
                     draw_fill.base.box.bottom - draw_fill.base.box.top);
                 document.getElementById(this.parent.dump_id).appendChild(debug_canvas);
             }
-                
+
             this.surfaces[draw_fill.base.surface_id].draw_count++;
 
         }
@@ -484,9 +481,9 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
             this.surfaces = [];
 
         var m = new SpiceMsgSurfaceCreate(msg.data);
-        DEBUG > 1 && console.log(this.type + ": MsgSurfaceCreate id " + m.surface.surface_id 
+        DEBUG > 1 && console.log(this.type + ": MsgSurfaceCreate id " + m.surface.surface_id
                                     + "; " + m.surface.width + "x" + m.surface.height
-                                    + "; format " + m.surface.format 
+                                    + "; format " + m.surface.format
                                     + "; flags " + m.surface.flags);
         if (m.surface.format != SPICE_SURFACE_FMT_32_xRGB &&
             m.surface.format != SPICE_SURFACE_FMT_32_ARGB)
@@ -535,7 +532,10 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
     if (msg.type == SPICE_MSG_DISPLAY_STREAM_CREATE)
     {
         var m = new SpiceMsgDisplayStreamCreate(msg.data);
-        DEBUG > 1 && console.log(this.type + ": MsgStreamCreate id" + m.id);
+        STREAM_DEBUG > 0 && console.log(this.type + ": MsgStreamCreate id" + m.id + "; type " + m.codec_type +
+                                        "; width " + m.stream_width + "; height " + m.stream_height +
+                                        "; left " + m.dest.left + "; top " + m.dest.top
+                                        );
         if (!this.streams)
             this.streams = new Array();
         if (this.streams[m.id])
@@ -567,14 +567,21 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
             media.addEventListener('sourceended', handle_video_source_ended, false);
             media.addEventListener('sourceclosed', handle_video_source_closed, false);
 
-            this.streams[m.id].video = v;
-            this.streams[m.id].media = media;
+            var s = this.streams[m.id];
+            s.video = v;
+            s.media = media;
+            s.queue = new Array();
+            s.start_time = 0;
+            s.cluster_time = 0;
+            s.append_okay = false;
 
-            media.stream = this.streams[m.id];
+            media.stream = s;
             media.spiceconn = this;
-            v.spice_stream = this.streams[m.id];
+            v.spice_stream = s;
         }
-        else if (m.codec_type != SPICE_VIDEO_CODEC_TYPE_MJPEG)
+        else if (m.codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG)
+            this.streams[m.id].frames_loading = 0;
+        else
             console.log("Unhandled stream codec: "+m.codec_type);
         return true;
     }
@@ -623,7 +630,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
     if (msg.type == SPICE_MSG_DISPLAY_STREAM_CLIP)
     {
         var m = new SpiceMsgDisplayStreamClip(msg.data);
-        DEBUG > 1 && console.log(this.type + ": MsgStreamClip id" + m.id);
+        STREAM_DEBUG > 1 && console.log(this.type + ": MsgStreamClip id" + m.id);
         this.streams[m.id].clip = m.clip;
         return true;
     }
@@ -631,7 +638,7 @@ SpiceDisplayConn.prototype.process_channel_message = function(msg)
     if (msg.type == SPICE_MSG_DISPLAY_STREAM_DESTROY)
     {
         var m = new SpiceMsgDisplayStreamDestroy(msg.data);
-        DEBUG > 1 && console.log(this.type + ": MsgStreamDestroy id" + m.id);
+        STREAM_DEBUG > 0 && console.log(this.type + ": MsgStreamDestroy id" + m.id);
 
         if (this.streams[m.id].codec_type == SPICE_VIDEO_CODEC_TYPE_VP8)
         {
@@ -745,7 +752,7 @@ SpiceDisplayConn.prototype.draw_copy_helper = function(o)
 SpiceDisplayConn.prototype.log_draw = function(prefix, draw)
 {
     var str = prefix + "." + draw.base.surface_id + "." + this.surfaces[draw.base.surface_id].draw_count + ": ";
-    str += "base.box " + draw.base.box.left + ", " + draw.base.box.top + " to " + 
+    str += "base.box " + draw.base.box.left + ", " + draw.base.box.top + " to " +
                            draw.base.box.right + ", " + draw.base.box.bottom;
     str += "; clip.type " + draw.base.clip.type;
 
@@ -762,13 +769,19 @@ SpiceDisplayConn.prototype.log_draw = function(prefix, draw)
             str += "; src_bitmap type " + draw.data.src_bitmap.descriptor.type + ", flags " + draw.data.src_bitmap.descriptor.flags;
             if (draw.data.src_bitmap.surface_id !== undefined)
                 str += "; src_bitmap surface_id " + draw.data.src_bitmap.surface_id;
+            if (draw.data.src_bitmap.bitmap)
+                str += "; BITMAP format " + draw.data.src_bitmap.bitmap.format +
+                        "; flags " + draw.data.src_bitmap.bitmap.flags +
+                        "; x " + draw.data.src_bitmap.bitmap.x +
+                        "; y " + draw.data.src_bitmap.bitmap.y +
+                        "; stride " + draw.data.src_bitmap.bitmap.stride ;
             if (draw.data.src_bitmap.quic)
-                str += "; QUIC type " + draw.data.src_bitmap.quic.type + 
-                        "; width " + draw.data.src_bitmap.quic.width + 
+                str += "; QUIC type " + draw.data.src_bitmap.quic.type +
+                        "; width " + draw.data.src_bitmap.quic.width +
                         "; height " + draw.data.src_bitmap.quic.height ;
             if (draw.data.src_bitmap.lz_rgb)
                 str += "; LZ_RGB length " + draw.data.src_bitmap.lz_rgb.length +
-                       "; magic " + draw.data.src_bitmap.lz_rgb.magic + 
+                       "; magic " + draw.data.src_bitmap.lz_rgb.magic +
                        "; version 0x" + draw.data.src_bitmap.lz_rgb.version.toString(16) +
                        "; type " + draw.data.src_bitmap.lz_rgb.type +
                        "; width " + draw.data.src_bitmap.lz_rgb.width +
@@ -876,6 +889,9 @@ function handle_draw_jpeg_onload()
     var temp_canvas = null;
     var context;
 
+    if (this.o.sc.streams[this.o.id])
+        this.o.sc.streams[this.o.id].frames_loading--;
+
     /*------------------------------------------------------------
     ** FIXME:
     **  The helper should be extended to be able to handle actual HtmlImageElements
@@ -885,7 +901,7 @@ function handle_draw_jpeg_onload()
     {
         // This can happen; if the jpeg image loads after our surface
         //  has been destroyed (e.g. open a menu, close it quickly),
-        //  we'll find we have no surface.  
+        //  we'll find we have no surface.
         DEBUG > 2 && this.o.sc.log_info("Discarding jpeg; presumed lost surface " + this.o.base.surface_id);
         temp_canvas = document.createElement("canvas");
         temp_canvas.setAttribute('width', this.o.base.box.right);
@@ -904,16 +920,16 @@ function handle_draw_jpeg_onload()
         t.putImageData(this.alpha_img, 0, 0);
         t.globalCompositeOperation = 'source-in';
         t.drawImage(this, 0, 0);
-     
+
         context.drawImage(c, this.o.base.box.left, this.o.base.box.top);
 
-        if (this.o.descriptor && 
+        if (this.o.descriptor &&
             (this.o.descriptor.flags & SPICE_IMAGE_FLAGS_CACHE_ME))
         {
             if (! ("cache" in this.o.sc))
                 this.o.sc.cache = {};
 
-            this.o.sc.cache[this.o.descriptor.id] = 
+            this.o.sc.cache[this.o.descriptor.id] =
                 t.getImageData(0, 0,
                     this.alpha_img.width,
                     this.alpha_img.height);
@@ -925,15 +941,16 @@ function handle_draw_jpeg_onload()
 
         // Give the Garbage collector a clue to recycle this; avoids
         //  fairly massive memory leaks during video playback
-        this.src = null;
+        this.onload = undefined;
+        this.src = EMPTY_GIF_IMAGE;
 
-        if (this.o.descriptor && 
+        if (this.o.descriptor &&
             (this.o.descriptor.flags & SPICE_IMAGE_FLAGS_CACHE_ME))
         {
             if (! ("cache" in this.o.sc))
                 this.o.sc.cache = {};
 
-            this.o.sc.cache[this.o.descriptor.id] = 
+            this.o.sc.cache[this.o.descriptor.id] =
                 context.getImageData(this.o.base.box.left, this.o.base.box.top,
                     this.o.base.box.right - this.o.base.box.left,
                     this.o.base.box.bottom - this.o.base.box.top);
@@ -955,13 +972,16 @@ function handle_draw_jpeg_onload()
         this.o.sc.surfaces[this.o.base.surface_id].draw_count++;
     }
 
-    if ("report" in this.o.sc.streams[this.o.id])
-            process_stream_data_report(this.o.sc, this.o.id, this.o.msg_mmtime, this.o.msg_mmtime - this.o.sc.parent.relative_now())
+    if (this.o.sc.streams[this.o.id] && "report" in this.o.sc.streams[this.o.id])
+        process_stream_data_report(this.o.sc, this.o.id, this.o.msg_mmtime, this.o.msg_mmtime - this.o.sc.parent.relative_now());
 }
 
 function process_mjpeg_stream_data(sc, m, time_until_due)
 {
-    if (time_until_due < 0)
+    /* If we are currently processing an mjpeg frame when a new one arrives,
+       and the new one is 'late', drop the new frame.  This helps the browsers
+       keep up, and provides rate control feedback as well */
+    if (time_until_due < 0 && sc.streams[m.base.id].frames_loading > 0)
     {
         if ("report" in sc.streams[m.base.id])
             sc.streams[m.base.id].report.num_drops++;
@@ -992,6 +1012,8 @@ function process_mjpeg_stream_data(sc, m, time_until_due)
         };
     img.onload = handle_draw_jpeg_onload;
     img.src = tmpstr;
+
+    sc.streams[m.base.id].frames_loading++;
 }
 
 function process_stream_data_report(sc, id, msg_mmtime, time_until_due)
@@ -1035,10 +1057,6 @@ function handle_video_source_open(e)
     s.spiceconn = p;
     s.stream = stream;
 
-    stream.queue = new Array();
-    stream.start_time = 0;
-    stream.cluster_time = 0;
-
     listen_for_video_events(stream);
 
     var h = new webm_Header();
@@ -1102,6 +1120,24 @@ function handle_append_video_buffer_done(e)
     {
         stream.append_okay = true;
     }
+
+    if (!stream.video)
+    {
+        if (STREAM_DEBUG > 0)
+            console.log("Stream id " + stream.id + " received updateend after video is gone.");
+        return;
+    }
+
+    if (stream.video.buffered.length > 0 &&
+        stream.video.currentTime < stream.video.buffered.start(stream.video.buffered.length - 1))
+    {
+        console.log("Video appears to have fallen behind; advancing to " +
+            stream.video.buffered.start(stream.video.buffered.length - 1));
+        stream.video.currentTime = stream.video.buffered.start(stream.video.buffered.length - 1);
+    }
+
+    if (STREAM_DEBUG > 1)
+        console.log(stream.video.currentTime + ":id " +  stream.id + " updateend " + dump_media_element(stream.video));
 }
 
 function handle_video_buffer_error(e)
@@ -1153,13 +1189,9 @@ function new_video_cluster(stream, msg)
 
 function process_video_stream_data(stream, msg)
 {
-    if (! stream.source_buffer)
-        return true;
-
     if (stream.start_time == 0)
     {
         stream.start_time = msg.base.multi_media_time;
-        stream.video.play();
         new_video_cluster(stream, msg);
     }
 
@@ -1185,7 +1217,7 @@ function video_handle_event_debug(e)
     if (STREAM_DEBUG > 1 && s.source_buffer)
         console.log("  source_buffer " + dump_source_buffer(s.source_buffer));
 
-    if (STREAM_DEBUG > 0 || s.queue.length > 1)
+    if (STREAM_DEBUG > 1 || s.queue.length > 1)
         console.log('  queue len ' + s.queue.length + '; append_okay: ' + s.append_okay);
 }
 
@@ -1203,10 +1235,11 @@ function listen_for_video_events(stream)
     var video_1_events = [
         "loadstart", "suspend", "emptied", "stalled", "loadedmetadata", "loadeddata", "canplay",
         "canplaythrough", "playing", "waiting", "seeking", "seeked", "ended", "durationchange",
-        "timeupdate", "play", "pause", "ratechange"
+        "play", "pause", "ratechange"
     ];
 
     var video_2_events = [
+        "timeupdate",
         "progress",
         "resize",
         "volumechange"
diff --git a/enums.js b/enums.js
index 3ef36dc..b6e013c 100644
--- a/enums.js
+++ b/enums.js
@@ -166,6 +166,15 @@ var SPICE_MSG_PLAYBACK_VOLUME           = 105;
 var SPICE_MSG_PLAYBACK_MUTE             = 106;
 var SPICE_MSG_PLAYBACK_LATENCY          = 107;
 
+var SPICE_MSG_SPICEVMC_DATA             = 101;
+var SPICE_MSG_PORT_INIT                 = 201;
+var SPICE_MSG_PORT_EVENT                = 202;
+var SPICE_MSG_END_PORT                  = 203;
+
+var SPICE_MSGC_SPICEVMC_DATA            = 101;
+var SPICE_MSGC_PORT_EVENT               = 201;
+var SPICE_MSGC_END_PORT                 = 202;
+
 var SPICE_PLAYBACK_CAP_CELT_0_5_1       = 0;
 var SPICE_PLAYBACK_CAP_VOLUME           = 1;
 var SPICE_PLAYBACK_CAP_LATENCY          = 2;
@@ -264,7 +273,7 @@ var SPICE_MOUSE_BUTTON_MASK_LEFT = (1 << 0),
     SPICE_MOUSE_BUTTON_MASK_MIDDLE = (1 << 1),
     SPICE_MOUSE_BUTTON_MASK_RIGHT = (1 << 2),
     SPICE_MOUSE_BUTTON_MASK_MASK = 0x7;
-    
+
 var SPICE_MOUSE_BUTTON_INVALID  = 0;
 var SPICE_MOUSE_BUTTON_LEFT     = 1;
 var SPICE_MOUSE_BUTTON_MIDDLE   = 2;
diff --git a/lz.js b/lz.js
index 4292eac..53c1141 100644
--- a/lz.js
+++ b/lz.js
@@ -141,6 +141,19 @@ function lz_rgb32_decompress(in_buf, at, out_buf, type, default_alpha)
     return encoder - 1;
 }
 
+function flip_image_data(img)
+{
+    var wb = img.width * 4;
+    var h = img.height;
+    var temp_h = h;
+    var buff = new Uint8Array(img.width * img.height * 4);
+    while (temp_h--)
+    {
+        buff.set(img.data.subarray(temp_h * wb, (temp_h + 1) * wb), (h - temp_h - 1) * wb);
+    }
+    img.data.set(buff);
+}
+
 function convert_spice_lz_to_web(context, lz_image)
 {
     var at;
@@ -150,6 +163,9 @@ function convert_spice_lz_to_web(context, lz_image)
         var ret = context.createImageData(lz_image.width, lz_image.height);
 
         at = lz_rgb32_decompress(u8, 0, ret.data, LZ_IMAGE_TYPE_RGB32, lz_image.type != LZ_IMAGE_TYPE_RGBA);
+        if (!lz_image.top_down)
+            flip_image_data(ret);
+
         if (lz_image.type == LZ_IMAGE_TYPE_RGBA)
             lz_rgb32_decompress(u8, at, ret.data, LZ_IMAGE_TYPE_RGBA, false);
     }
diff --git a/main.js b/main.js
index afe69bf..6976f9c 100644
--- a/main.js
+++ b/main.js
@@ -22,7 +22,7 @@
 **  SpiceMainConn
 **      This is the master Javascript class for establishing and
 **  managing a connection to a Spice Server.
-**  
+**
 **      Invocation:  You must pass an object with properties as follows:
 **          uri         (required)  Uri of a WebSocket listener that is
 **                                  connected to a spice server.
@@ -59,6 +59,7 @@ function SpiceMainConn()
     this.file_xfer_tasks = {};
     this.file_xfer_task_id = 0;
     this.file_xfer_read_queue = [];
+    this.ports = [];
 }
 
 SpiceMainConn.prototype = Object.create(SpiceConn.prototype);
@@ -144,7 +145,13 @@ SpiceMainConn.prototype.process_channel_message = function(msg)
                         chan_id : chans.channels[i].id
                     };
             if (chans.channels[i].type == SPICE_CHANNEL_DISPLAY)
-                this.display = new SpiceDisplayConn(conn);
+            {
+                if (chans.channels[i].id == 0) {
+                    this.display = new SpiceDisplayConn(conn);
+                } else {
+                    this.log_warn("The spice-html5 client does not handle multiple heads.");
+                }
+            }
             else if (chans.channels[i].type == SPICE_CHANNEL_INPUTS)
             {
                 this.inputs = new SpiceInputsConn(conn);
@@ -154,6 +161,8 @@ SpiceMainConn.prototype.process_channel_message = function(msg)
                 this.cursor = new SpiceCursorConn(conn);
             else if (chans.channels[i].type == SPICE_CHANNEL_PLAYBACK)
                 this.cursor = new SpicePlaybackConn(conn);
+            else if (chans.channels[i].type == SPICE_CHANNEL_PORT)
+                this.ports.push(new SpicePortConn(conn));
             else
             {
                 if (! ("extra_channels" in this))
diff --git a/playback.js b/playback.js
index 53b6983..5af9233 100644
--- a/playback.js
+++ b/playback.js
@@ -29,8 +29,6 @@ function SpicePlaybackConn()
     this.queue = new Array();
     this.append_okay = false;
     this.start_time = 0;
-    this.skip_until = 0;
-    this.gap_time = 0;
 }
 
 SpicePlaybackConn.prototype = Object.create(SpiceConn.prototype);
@@ -91,56 +89,59 @@ SpicePlaybackConn.prototype.process_channel_message = function(msg)
     {
         var data = new SpiceMsgPlaybackData(msg.data);
 
-        // If this packet has the same time as the last, just bump up by one.
-        if (this.last_data_time && data.time <= this.last_data_time)
+        if (! this.source_buffer)
+            return true;
+
+        if (this.audio.readyState >= 3 && this.audio.buffered.length > 1 &&
+            this.audio.currentTime == this.audio.buffered.end(0) &&
+            this.audio.currentTime < this.audio.buffered.start(this.audio.buffered.length - 1))
         {
-            // FIXME - this is arguably wrong.  But delaying the transmission was worse,
-            //          in initial testing.  Could use more research.
-            PLAYBACK_DEBUG > 1 && console.log("Hacking time of " + data.time + " to " + this.last_data_time + 1);
-            data.time = this.last_data_time + 1;
+            console.log("Audio underrun: we appear to have fallen behind; advancing to " +
+                this.audio.buffered.start(this.audio.buffered.length - 1));
+            this.audio.currentTime = this.audio.buffered.start(this.audio.buffered.length - 1);
         }
 
-        if (! this.source_buffer)
-            return true;
+        /* Around version 45, Firefox started being very particular about the
+           time stamps put into the Opus stream.  The time stamps from the Spice server are
+           somewhat irregular.  They mostly arrive every 10 ms, but sometimes it is 11, or sometimes
+           with two time stamps the same in a row.  The previous logic resulted in fuzzy and/or
+           distorted audio streams in Firefox in a row.
 
-        /* Gap detection:  If there has been a delay since our last packet, then audio must
-             have paused.  Handling that gets tricky.  In Chrome, you can seek forward,
-             but you cannot in Firefox.  And seeking forward in Chrome is nice, as it keeps
-             Chrome from being overly cautious in it's buffer strategy.
+           In theory, the sequence mode should be appropriate for us, but as of 09/27/2016,
+           I was unable to make sequence mode work with Firefox.
 
-             So we do two things.  First, we seek forward.  Second, we compute how much of a gap
-             there would have been, and essentially eliminate it.
+           Thus, we end up with an inelegant hack.  Essentially, we force every packet to have
+           a 10ms time delta, unless there is an obvious gap in time stream, in which case we
+           will resync.
         */
-        if (this.last_data_time && data.time >= (this.last_data_time + GAP_DETECTION_THRESHOLD))
+
+        if (this.start_time != 0 && data.time != (this.last_data_time + EXPECTED_PACKET_DURATION))
         {
-            this.skip_until = data.time;
-            this.gap_time = (data.time - this.start_time) - 
-              (this.source_buffer.buffered.end(this.source_buffer.buffered.end.length - 1) * 1000.0).toFixed(0);
+            if (Math.abs(data.time - (EXPECTED_PACKET_DURATION + this.last_data_time)) < MAX_CLUSTER_TIME)
+            {
+                PLAYBACK_DEBUG > 1 && console.log("Hacking time of " + data.time + " to " +
+                                      (this.last_data_time + EXPECTED_PACKET_DURATION));
+                data.time = this.last_data_time + EXPECTED_PACKET_DURATION;
+            }
+            else
+            {
+                PLAYBACK_DEBUG > 1 && console.log("Apparent gap in audio time; now is " + data.time + " last was " + this.last_data_time);
+            }
         }
 
         this.last_data_time = data.time;
 
-
         PLAYBACK_DEBUG > 1 && console.log("PlaybackData; time " + data.time + "; length " + data.data.byteLength);
 
         if (this.start_time == 0)
             this.start_playback(data);
 
-        else if (data.time - this.cluster_time >= MAX_CLUSTER_TIME || this.skip_until > 0)
+        else if (data.time - this.cluster_time >= MAX_CLUSTER_TIME)
             this.new_cluster(data);
 
         else
             this.simple_block(data, false);
 
-        if (this.skip_until > 0)
-        {
-            this.audio.currentTime = (this.skip_until - this.start_time - this.gap_time) / 1000.0;
-            this.skip_until = 0;
-        }
-
-        if (this.audio.paused)
-            this.audio.play();
-
         return true;
     }
 
@@ -157,7 +158,22 @@ SpicePlaybackConn.prototype.process_channel_message = function(msg)
 
     if (msg.type == SPICE_MSG_PLAYBACK_STOP)
     {
-        return true;
+        PLAYBACK_DEBUG > 0 && console.log("PlaybackStop");
+        if (this.source_buffer)
+        {
+            document.getElementById(this.parent.screen_id).removeChild(this.audio);
+            window.URL.revokeObjectURL(this.audio.src);
+
+            delete this.source_buffer;
+            delete this.media_source;
+            delete this.audio;
+
+            this.append_okay = false;
+            this.queue = new Array();
+            this.start_time = 0;
+
+            return true;
+        }
     }
 
     if (msg.type == SPICE_MSG_PLAYBACK_VOLUME)
@@ -205,7 +221,7 @@ SpicePlaybackConn.prototype.new_cluster = function(data)
 {
     this.cluster_time = data.time;
 
-    var c = new webm_Cluster(data.time - this.start_time - this.gap_time);
+    var c = new webm_Cluster(data.time - this.start_time);
 
     var mb = new ArrayBuffer(c.buffer_size());
     this.bytes_written += c.to_buffer(mb);
@@ -273,6 +289,28 @@ function handle_source_closed(e)
     p.log_err('Audio source unexpectedly closed.');
 }
 
+function condense_playback_queue(queue)
+{
+    if (queue.length == 1)
+        return queue.shift();
+
+    var len = 0;
+    var i = 0;
+    for (i = 0; i < queue.length; i++)
+        len += queue[i].byteLength;
+
+    var mb = new ArrayBuffer(len);
+    var tmp = new Uint8Array(mb);
+    len = 0;
+    for (i = 0; i < queue.length; i++)
+    {
+        tmp.set(new Uint8Array(queue[i]), len);
+        len += queue[i].byteLength;
+    }
+    queue.length = 0;
+    return mb;
+}
+
 function handle_append_buffer_done(e)
 {
     var p = this.spiceconn;
@@ -282,7 +320,7 @@ function handle_append_buffer_done(e)
 
     if (p.queue.length > 0)
     {
-        var mb = p.queue.shift();
+        var mb = condense_playback_queue(p.queue);
         playback_append_buffer(p, mb);
     }
     else
diff --git a/port.js b/port.js
new file mode 100644
index 0000000..ee22073
--- /dev/null
+++ b/port.js
@@ -0,0 +1,85 @@
+"use strict";
+/*
+   Copyright (C) 2016 by Oliver Gutierrez <ogutsua@gmail.com>
+                         Miroslav Chodil <mchodil@redhat.com>
+
+   This file is part of spice-html5.
+
+   spice-html5 is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   spice-html5 is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with spice-html5.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*----------------------------------------------------------------------------
+**  SpicePortConn
+**      Drive the Spice Port Channel
+**--------------------------------------------------------------------------*/
+function SpicePortConn()
+{
+    DEBUG > 0 && console.log('SPICE port: created SPICE port channel. Args:', arguments);
+    SpiceConn.apply(this, arguments);
+    this.port_name = null;
+}
+
+SpicePortConn.prototype = Object.create(SpiceConn.prototype);
+
+SpicePortConn.prototype.process_channel_message = function(msg)
+{
+    if (msg.type == SPICE_MSG_PORT_INIT)
+    {
+        if (this.port_name === null)
+        {
+            var m = new SpiceMsgPortInit(msg.data);
+            this.portName = arraybuffer_to_str(new Uint8Array(m.name));
+            this.portOpened = m.opened
+            DEBUG > 0 && console.log('SPICE port: Port', this.portName, 'initialized');
+            return true;
+        }
+
+        DEBUG > 0 && console.log('SPICE port: Port', this.port_name, 'is already initialized.');
+    }
+    else if (msg.type == SPICE_MSG_PORT_EVENT)
+    {
+        DEBUG > 0 && console.log('SPICE port: Port event received for', this.portName, msg);
+        var event = new CustomEvent('spice-port-event', {
+            detail: {
+                channel: this,
+                spiceEvent: new Uint8Array(msg.data)
+            },
+            bubbles: true,
+            cancelable: true
+        });
+
+        window.dispatchEvent(event);
+        return true;
+    }
+    else if (msg.type == SPICE_MSG_SPICEVMC_DATA)
+    {
+        DEBUG > 0 && console.log('SPICE port: Data received in port', this.portName, msg);
+        var event = new CustomEvent('spice-port-data', {
+            detail: {
+                channel: this,
+                data: msg.data
+            },
+            bubbles: true,
+            cancelable: true
+        });
+        window.dispatchEvent(event);
+        return true;
+    }
+    else
+    {
+        DEBUG > 0 && console.log('SPICE port: SPICE message type not recognized:', msg)
+    }
+
+    return false;
+};
diff --git a/resize.js b/resize.js
index f5410d3..51fb1cc 100644
--- a/resize.js
+++ b/resize.js
@@ -33,17 +33,29 @@
 function resize_helper(sc)
 {
     var w = document.getElementById(sc.screen_id).clientWidth;
-    var h = document.getElementById(sc.screen_id).clientHeight;
-
     var m = document.getElementById(sc.message_id);
 
     /* Resize vertically; basically we leave a 20 pixel margin
          at the bottom, and use the position of the message window
          to figure out how to resize */
-    var hd = window.innerHeight - m.offsetHeight - m.offsetTop - 20;
+
+    var h = window.innerHeight - 20;
+
+    /* Screen height based on debug console visibility  */
+    if (window.getComputedStyle(m).getPropertyValue("display") == 'none')
+    {
+        /* Get console height from spice.css .spice-message */
+        var mh = parseInt(window.getComputedStyle(m).getPropertyValue("height"), 10);
+        h = h - mh;
+    }
+    else
+    {
+        /* Show both div elements - spice-area and message-div */
+        h = h - m.offsetHeight - m.clientHeight;
+    }
+
 
     /* Xorg requires height be a multiple of 8; round up */
-    h = h + hd;
     if (h % 8 > 0)
         h += (8 - (h % 8));
 
diff --git a/simulatecursor.js b/simulatecursor.js
index b1fce06..ffd9089 100644
--- a/simulatecursor.js
+++ b/simulatecursor.js
@@ -71,7 +71,7 @@ simulate_cursor: function (spicecursor, cursor, screen, pngstr)
 
     if (window.getComputedStyle(screen, null).cursor == 'auto')
     {
-        SpiceSimulateCursor.unknown_cursor(cursor_sha, 
+        SpiceSimulateCursor.unknown_cursor(cursor_sha,
             SpiceSimulateCursor.create_icondir(cursor.header.width, cursor.header.height,
             cursor.data.byteLength, cursor.header.hot_spot_x, cursor.header.hot_spot_y) + pngstr);
 
@@ -99,7 +99,7 @@ simulate_cursor: function (spicecursor, cursor, screen, pngstr)
         spicecursor.spice_simulated_cursor.style.pointerEvents = "none";
     }
     else
-    { 
+    {
         if (spicecursor.spice_simulated_cursor)
         {
             spicecursor.spice_simulated_cursor.spice_screen.removeChild(spicecursor.spice_simulated_cursor);
@@ -162,7 +162,7 @@ create_icondir: function (width, height, bytes, hot_x, hot_y)
 
 };
 
-SpiceSimulateCursor.ICONDIR.prototype = 
+SpiceSimulateCursor.ICONDIR.prototype =
 {
     to_buffer: function(a, at)
     {
diff --git a/spice.css b/spice.css
index 5d092ba..ee1b2f3 100644
--- a/spice.css
+++ b/spice.css
@@ -115,4 +115,3 @@ body
 .spice-message-error {
     color: red;
 }
-
diff --git a/spice.html b/spice.html
index f2f9ed0..7abfcff 100644
--- a/spice.html
+++ b/spice.html
@@ -28,26 +28,27 @@
     <head>
 
         <title>Spice Javascript client</title>
-        <script src="spicearraybuffer.js"></script> 
-        <script src="enums.js"></script> 
-        <script src="atKeynames.js"></script> 
-        <script src="utils.js"></script> 
-        <script src="png.js"></script> 
-        <script src="lz.js"></script> 
-        <script src="quic.js"></script> 
-        <script src="bitmap.js"></script> 
-        <script src="spicedataview.js"></script> 
-        <script src="spicetype.js"></script> 
-        <script src="spicemsg.js"></script> 
-        <script src="wire.js"></script> 
-        <script src="spiceconn.js"></script> 
-        <script src="display.js"></script> 
-        <script src="main.js"></script> 
-        <script src="inputs.js"></script> 
+        <script src="spicearraybuffer.js"></script>
+        <script src="enums.js"></script>
+        <script src="atKeynames.js"></script>
+        <script src="utils.js"></script>
+        <script src="png.js"></script>
+        <script src="lz.js"></script>
+        <script src="quic.js"></script>
+        <script src="bitmap.js"></script>
+        <script src="spicedataview.js"></script>
+        <script src="spicetype.js"></script>
+        <script src="spicemsg.js"></script>
+        <script src="wire.js"></script>
+        <script src="spiceconn.js"></script>
+        <script src="display.js"></script>
+        <script src="port.js"></script>
+        <script src="main.js"></script>
+        <script src="inputs.js"></script>
         <script src="webm.js"></script>
         <script src="playback.js"></script>
         <script src="simulatecursor.js"></script>
-        <script src="cursor.js"></script> 
+        <script src="cursor.js"></script>
         <script src="thirdparty/jsbn.js"></script>
         <script src="thirdparty/rsa.js"></script>
         <script src="thirdparty/prng4.js"></script>
@@ -71,8 +72,8 @@
             {
                 var host, port, password, scheme = "ws://", uri;
 
-                host = document.getElementById("host").value; 
-                port = document.getElementById("port").value; 
+                host = document.getElementById("host").value;
+                port = document.getElementById("port").value;
                 password = document.getElementById("password").value;
 
 
@@ -92,7 +93,7 @@
 
                 try
                 {
-                    sc = new SpiceMainConn({uri: uri, screen_id: "spice-screen", dump_id: "debug-div", 
+                    sc = new SpiceMainConn({uri: uri, screen_id: "spice-screen", dump_id: "debug-div",
                                 message_id: "message-div", password: password, onerror: spice_error, onagent: agent_connected });
                 }
                 catch (e)
@@ -142,6 +143,35 @@
                 }
             }
 
+            function toggle_console()
+            {
+                var checkbox = document.getElementById('show_console');
+                var m = document.getElementById('message-div');
+
+                if (checkbox.checked)
+                {
+                    m.style.display = 'block';
+                }
+                else
+                {
+                    m.style.display = 'none';
+                }
+
+                window.addEventListener('resize', handle_resize);
+                resize_helper(sc);
+            }
+            /* SPICE port event listeners
+            window.addEventListener('spice-port-data', function(event) {
+                // Here we convert data to text, but really we can obtain binary data also
+                var msg_text = arraybuffer_to_str(new Uint8Array(event.detail.data));
+                DEBUG > 0 && console.log('SPICE port', event.detail.channel.portName, 'message text:', msg_text);
+            });
+
+            window.addEventListener('spice-port-event', function(event) {
+                DEBUG > 0 && console.log('SPICE port', event.detail.channel.portName, 'event data:', event.detail.spiceEvent);
+            });
+            */
+
         </script>
 
     </head>
@@ -153,6 +183,7 @@
             <label for="host">Host:</label> <input type='text' id='host' value='localhost'> <!-- localhost -->
             <label for="port">Port:</label> <input type='text' id='port' value='5959'>
             <label for="password">Password:</label> <input type='password' id='password' value=''>
+            <label for="show_console">Show console </label><input type="checkbox" id="show_console" value="1" onchange="toggle_console()" checked>
             <button id="connectButton" onclick="connect();">Start</button>
         </div>
 
diff --git a/spice_auto.html b/spice_auto.html
index 9aae118..2f04fc9 100644
--- a/spice_auto.html
+++ b/spice_auto.html
@@ -28,7 +28,7 @@
     <head>
 
         <title>Spice Javascript client</title>
-        <script src="spicearraybuffer.js"></script> 
+        <script src="spicearraybuffer.js"></script>
         <script src="enums.js"></script>
         <script src="atKeynames.js"></script>
         <script src="utils.js"></script>
@@ -42,6 +42,7 @@
         <script src="wire.js"></script>
         <script src="spiceconn.js"></script>
         <script src="display.js"></script>
+        <script src="port.js"></script>
         <script src="main.js"></script>
         <script src="inputs.js"></script>
         <script src="webm.js"></script>
@@ -182,6 +183,18 @@
                 }
             }
 
+            /* SPICE port event listeners
+            window.addEventListener('spice-port-data', function(event) {
+                // Here we convert data to text, but really we can obtain binary data also
+                var msg_text = arraybuffer_to_str(new Uint8Array(event.detail.data));
+                DEBUG > 0 && console.log('SPICE port', event.detail.channel.portName, 'message text:', msg_text);
+            });
+
+            window.addEventListener('spice-port-event', function(event) {
+                DEBUG > 0 && console.log('SPICE port', event.detail.channel.portName, 'event data:', event.detail.spiceEvent);
+            });
+            */
+
             connect();
         </script>
 
diff --git a/spiceconn.js b/spiceconn.js
index 41d2fa3..33e7388 100644
--- a/spiceconn.js
+++ b/spiceconn.js
@@ -23,7 +23,7 @@
 **      This is the base Javascript class for establishing and
 **  managing a connection to a Spice Server.
 **  It is used to provide core functionality to the Spice main,
-**  display, inputs, and cursor channels.  See main.js for 
+**  display, inputs, and cursor channels.  See main.js for
 **  usage.
 **--------------------------------------------------------------------------*/
 function SpiceConn(o)
@@ -119,28 +119,36 @@ SpiceConn.prototype =
 
         msg.connection_id = this.connection_id;
         msg.channel_type = this.type;
-        // FIXME - we're not setting a channel_id...
+        msg.channel_id = this.chan_id;
+
         msg.common_caps.push(
             (1 << SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION) |
             (1 << SPICE_COMMON_CAP_MINI_HEADER)
             );
 
         if (msg.channel_type == SPICE_CHANNEL_PLAYBACK)
-            msg.channel_caps.push(
-                (1 << SPICE_PLAYBACK_CAP_OPUS)
-            );
+        {
+            var caps = 0;
+            if ('MediaSource' in window && MediaSource.isTypeSupported(SPICE_PLAYBACK_CODEC))
+                caps |= (1 << SPICE_PLAYBACK_CAP_OPUS);
+            msg.channel_caps.push(caps);
+        }
         else if (msg.channel_type == SPICE_CHANNEL_MAIN)
+        {
             msg.channel_caps.push(
                 (1 << SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS)
             );
+        }
         else if (msg.channel_type == SPICE_CHANNEL_DISPLAY)
-            msg.channel_caps.push(
-                (1 << SPICE_DISPLAY_CAP_SIZED_STREAM) |
-                (1 << SPICE_DISPLAY_CAP_STREAM_REPORT) |
-                (1 << SPICE_DISPLAY_CAP_MULTI_CODEC) |
-                (1 << SPICE_DISPLAY_CAP_CODEC_MJPEG) |
-                (1 << SPICE_DISPLAY_CAP_CODEC_VP8)
-            );
+        {
+            var caps =  (1 << SPICE_DISPLAY_CAP_SIZED_STREAM) |
+                        (1 << SPICE_DISPLAY_CAP_STREAM_REPORT) |
+                        (1 << SPICE_DISPLAY_CAP_MULTI_CODEC) |
+                        (1 << SPICE_DISPLAY_CAP_CODEC_MJPEG);
+            if ('MediaSource' in window && MediaSource.isTypeSupported(SPICE_VP8_CODEC))
+                caps |= (1 << SPICE_DISPLAY_CAP_CODEC_VP8);
+            msg.channel_caps.push(caps);
+        }
 
         hdr.size = msg.buffer_size();
 
@@ -188,8 +196,11 @@ SpiceConn.prototype =
 
                 if (msg.type > 500)
                 {
-                    alert("Something has gone very wrong; we think we have message of type " + msg.type);
-                    debugger;
+                    if (DEBUG > 0)
+                    {
+                        alert("Something has gone very wrong; we think we have message of type " + msg.type);
+                        debugger;
+                    }
                 }
 
                 if (msg.size == 0)
diff --git a/spicedataview.js b/spicedataview.js
index 800df03..719d968 100644
--- a/spicedataview.js
+++ b/spicedataview.js
@@ -20,10 +20,10 @@
 
 /*----------------------------------------------------------------------------
 **  SpiceDataView
-** FIXME FIXME 
+** FIXME FIXME
 **    This is used because Firefox does not have DataView yet.
-**    We should use DataView if we have it, because it *has* to 
-**    be faster than this code 
+**    We should use DataView if we have it, because it *has* to
+**    be faster than this code
 **--------------------------------------------------------------------------*/
 function SpiceDataView(buffer, byteOffset, byteLength)
 {
@@ -63,7 +63,7 @@ SpiceDataView.prototype = {
             high = 2;
         }
 
-        return (this.getUint16(byteOffset + high, littleEndian) << 16) | 
+        return (this.getUint16(byteOffset + high, littleEndian) << 16) |
                 this.getUint16(byteOffset + low, littleEndian);
     },
     getUint64: function (byteOffset, littleEndian)
diff --git a/spicemsg.js b/spicemsg.js
index db6625a..3619996 100644
--- a/spicemsg.js
+++ b/spicemsg.js
@@ -21,7 +21,7 @@
 /*----------------------------------------------------------------------------
 **  Spice messages
 **      This file contains classes for passing messages to and from
-**  a spice server.  This file should arguably be generated from 
+**  a spice server.  This file should arguably be generated from
 **  spice.proto, but it was instead put together by hand.
 **--------------------------------------------------------------------------*/
 function SpiceLinkHeader(a, at)
@@ -63,7 +63,7 @@ SpiceLinkHeader.prototype =
         dv.setUint32(at, this.size, true); at += 4;
     },
     buffer_size: function()
-    { 
+    {
         return 16;
     },
 }
@@ -938,7 +938,7 @@ function SpiceMsgcMousePosition(sc, e)
         this.x = e.clientX - sc.display.surfaces[sc.display.primary_surface].canvas.offsetLeft + scrollLeft;
         this.y = e.clientY - sc.display.surfaces[sc.display.primary_surface].canvas.offsetTop + scrollTop;
         sc.mousex = this.x;
-        sc.mousey = this.y; 
+        sc.mousey = this.y;
     }
     else
     {
@@ -1278,3 +1278,21 @@ SpiceMsgDisplayInvalList.prototype =
         }
     },
 }
+
+function SpiceMsgPortInit(a, at)
+{
+    this.from_buffer(a,at);
+};
+
+SpiceMsgPortInit.prototype =
+{
+    from_buffer: function (a, at)
+    {
+        at = at || 0;
+        var dv = new SpiceDataView(a);
+        var namesize = dv.getUint32(at, true); at += 4;
+        var offset = dv.getUint32(at, true); at += 4;
+        this.opened = dv.getUint8(at, true); at += 1;
+        this.name = a.slice(offset, offset + namesize - 1);
+    }
+}
diff --git a/spicetype.js b/spicetype.js
index 951b277..e145abc 100644
--- a/spicetype.js
+++ b/spicetype.js
@@ -469,5 +469,5 @@ SpiceSurface.prototype =
     },
 }
 
-/* FIXME - SpiceImage  types lz_plt, jpeg, zlib_glz, and jpeg_alpha are 
+/* FIXME - SpiceImage  types lz_plt, jpeg, zlib_glz, and jpeg_alpha are
            completely unimplemented */
diff --git a/thirdparty/jsbn.js b/thirdparty/jsbn.js
index 9b9476e..d88ec54 100644
--- a/thirdparty/jsbn.js
+++ b/thirdparty/jsbn.js
@@ -15,9 +15,9 @@
  * The above copyright notice and this permission notice shall be
  * included in all copies or substantial portions of the Software.
  *
- * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
- * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
- * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
+ * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+ * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
  *
  * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
  * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
diff --git a/thirdparty/prng4.js b/thirdparty/prng4.js
index 4715372..ef3efd6 100644
--- a/thirdparty/prng4.js
+++ b/thirdparty/prng4.js
@@ -15,9 +15,9 @@
  * The above copyright notice and this permission notice shall be
  * included in all copies or substantial portions of the Software.
  *
- * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
- * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
- * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
+ * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+ * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
  *
  * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
  * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
diff --git a/thirdparty/rng.js b/thirdparty/rng.js
index 829a23c..efbf382 100644
--- a/thirdparty/rng.js
+++ b/thirdparty/rng.js
@@ -15,9 +15,9 @@
  * The above copyright notice and this permission notice shall be
  * included in all copies or substantial portions of the Software.
  *
- * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
- * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
- * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
+ * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+ * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
  *
  * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
  * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
@@ -66,7 +66,7 @@ if(rng_pool == null) {
     var z = window.crypto.random(32);
     for(t = 0; t < z.length; ++t)
       rng_pool[rng_pptr++] = z.charCodeAt(t) & 255;
-  }  
+  }
   while(rng_pptr < rng_psize) {  // extract some randomness from Math.random()
     t = Math.floor(65536 * Math.random());
     rng_pool[rng_pptr++] = t >>> 8;
diff --git a/thirdparty/rsa.js b/thirdparty/rsa.js
index 1bbf249..ea0e45b 100644
--- a/thirdparty/rsa.js
+++ b/thirdparty/rsa.js
@@ -15,9 +15,9 @@
  * The above copyright notice and this permission notice shall be
  * included in all copies or substantial portions of the Software.
  *
- * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
- * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
- * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
+ * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+ * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
  *
  * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
  * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
diff --git a/utils.js b/utils.js
index 9093a24..7aeefdb 100644
--- a/utils.js
+++ b/utils.js
@@ -27,6 +27,11 @@ var STREAM_DEBUG = 0;
 var DUMP_DRAWS = false;
 var DUMP_CANVASES = false;
 
+/*----------------------------------------------------------------------------
+**  We use an Image temporarily, and the image/src does not get garbage
+**   collected as quickly as we might like.  This blank image helps with that.
+**--------------------------------------------------------------------------*/
+var EMPTY_GIF_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
 
 /*----------------------------------------------------------------------------
 **  combine_array_buffers
@@ -100,6 +105,13 @@ function hexdump_buffer(a)
 }
 
 /*----------------------------------------------------------------------------
+**  Convert arraybuffer to string
+**--------------------------------------------------------------------------*/
+function arraybuffer_to_str(buf) {
+  return String.fromCharCode.apply(null, new Uint16Array(buf));
+}
+
+/*----------------------------------------------------------------------------
 ** Converting keycodes to AT scancodes is very hard.
 ** luckly there are some resources on the web and in the Xorg driver that help
 ** us figure out what browser dependent keycodes match to what scancodes.
diff --git a/webm.js b/webm.js
index 8faa8e7..789da14 100644
--- a/webm.js
+++ b/webm.js
@@ -84,6 +84,7 @@ var OPUS_CHANNELS                           = 2;
 var SPICE_PLAYBACK_CODEC                    = 'audio/webm; codecs="opus"';
 var MAX_CLUSTER_TIME                        = 1000;
 
+var EXPECTED_PACKET_DURATION                = 10;
 var GAP_DETECTION_THRESHOLD                 = 50;
 
 var SPICE_VP8_CODEC                         = 'video/webm; codecs="vp8"';
diff --git a/wire.js b/wire.js
index 7407ce7..2c7f096 100644
--- a/wire.js
+++ b/wire.js
@@ -96,7 +96,7 @@ SpiceWireReader.prototype =
             this.callback.call(this.sc, mb,
                         this.saved_msg_header || undefined);
         }
-        
+
     },
 
     request: function(n)
 
projeto & código: Vladimir Lettiev aka crux © 2004-2005, Andrew Avramenko aka liks © 2007-2008
mantenedor atual: Michael Shigorin
mantenedor da tradução: Fernando Martini aka fmartini © 2009