diff --git a/src/html5/include/xpra_client.js b/src/html5/include/xpra_client.js
new file mode 100644
index 0000000000..1f070987aa
--- /dev/null
+++ b/src/html5/include/xpra_client.js
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2013 Antoine Martin
+ * Copyright (c) 2014 Joshua Higgins
+ * Copyright (c) 2015 Spikes, Inc.
+ * Licensed under MPL 2.0
+ *
+ * xpra client
+ *
+ * requires:
+ * xpra_protocol.js
+ * keycodes.js
+ */
+
+XPRA_CLIENT_FORCE_NO_WORKER = true;
+
+function XpraClient(container) {
+ // state
+ this.host = null;
+ this.port = null;
+ this.ssl = null;
+ // the container div is the "screen" on the HTML page where we
+ // are able to draw our windows in.
+ this.container = document.getElementById(container);
+ if(!this.container) {
+ throw "invalid container element";
+ }
+ // a list of our windows
+ this.id_to_window = {};
+ // the protocol
+ this.protocol = null;
+ // the client holds a list of packet handlers
+ this.packet_handlers = {
+ 'open': this._process_open,
+ 'ping': this._process_ping
+ };
+ // some client stuff
+ this.OLD_ENCODING_NAMES_TO_NEW = {"x264" : "h264", "vpx" : "vp8"};
+ this.RGB_FORMATS = ["RGBX", "RGBA"];
+}
+
+XpraClient.prototype.connect = function(host, port, ssl) {
+ // open the web socket, started it in a worker if available
+ console.log("connecting to xpra server " + host + ":" + port + " with ssl: " + ssl);
+ this.host = host;
+ this.port = port;
+ this.ssl = ssl;
+ // detect websocket in webworker support and degrade gracefully
+ if(window.Worker) {
+ console.log("we have webworker support");
+ // spawn worker that checks for a websocket
+ var me = this;
+ var worker = new Worker('include/wsworker_check.js');
+ worker.addEventListener('message', function(e) {
+ var data = e.data;
+ switch (data['result']) {
+ case true:
+ // yey, we can use websocket in worker!
+ console.log("we can use websocket in webworker");
+ me._do_connect(true);
+ break;
+ case false:
+ console.log("we can't use websocket in webworker, won't use webworkers");
+ break;
+ default:
+ console.log("client got unknown message from worker");
+ };
+ }, false);
+ // ask the worker to check for websocket support, when we recieve a reply
+ // through the eventlistener above, _do_connect() will finish the job
+ worker.postMessage({'cmd': 'check'});
+ } else {
+ // no webworker support
+ console.log("no webworker support at all.")
+ }
+}
+
+XpraClient.prototype._do_connect = function(with_worker) {
+ if(with_worker && !(XPRA_CLIENT_FORCE_NO_WORKER)) {
+ this.protocol = new XpraProtocolWorkerHost();
+ } else {
+ this.protocol = new XpraProtocol();
+ }
+ // set protocol to deliver packets to our packet router
+ this.protocol.set_packet_handler(this._route_packet, this);
+ // make uri
+ var uri = "ws://";
+ if (this.ssl)
+ uri = "wss://";
+ uri += this.host;
+ uri += ":" + this.port;
+ // do open
+ this.protocol.open(uri);
+}
+
+XpraClient.prototype._route_packet = function(packet, ctx) {
+ // ctx refers to `this` because we came through a callback
+ var packet_type = "";
+ var fn = "";
+ try {
+ packet_type = packet[0];
+ console.log("received a " + packet_type + " packet");
+ fn = ctx.packet_handlers[packet_type];
+ if (fn==undefined)
+ console.error("no packet handler for "+packet_type+"!");
+ else
+ fn(packet, ctx);
+ }
+ catch (e) {
+ console.error("error processing '"+packet_type+"' with '"+fn+"': "+e);
+ throw e;
+ }
+}
+
+XpraClient.prototype._guess_platform_processor = function() {
+ //mozilla property:
+ if (navigator.oscpu)
+ return navigator.oscpu;
+ //ie:
+ if (navigator.cpuClass)
+ return navigator.cpuClass;
+ return "unknown";
+}
+
+XpraClient.prototype._guess_platform_name = function() {
+ //use python style strings for platforms:
+ if (navigator.appVersion.indexOf("Win")!=-1)
+ return "Microsoft Windows";
+ if (navigator.appVersion.indexOf("Mac")!=-1)
+ return "Mac OSX";
+ if (navigator.appVersion.indexOf("Linux")!=-1)
+ return "Linux";
+ if (navigator.appVersion.indexOf("X11")!=-1)
+ return "Posix";
+ return "unknown";
+}
+
+XpraClient.prototype._guess_platform = function() {
+ //use python style strings for platforms:
+ if (navigator.appVersion.indexOf("Win")!=-1)
+ return "win32";
+ if (navigator.appVersion.indexOf("Mac")!=-1)
+ return "darwin";
+ if (navigator.appVersion.indexOf("Linux")!=-1)
+ return "linux2";
+ if (navigator.appVersion.indexOf("X11")!=-1)
+ return "posix";
+ return "unknown";
+}
+
+XpraClient.prototype._get_keyboard_layout = function() {
+ //IE:
+ //navigator.systemLanguage
+ //navigator.browserLanguage
+ var v = window.navigator.userLanguage || window.navigator.language;
+ //ie: v="en_GB";
+ v = v.split(",")[0];
+ var l = v.split("-", 2);
+ if (l.length==1)
+ l = v.split("_", 2);
+ if (l.length==1)
+ return "";
+ //ie: "gb"
+ return l[1].toLowerCase();
+}
+
+XpraClient.prototype._get_keycodes = function() {
+ //keycodes.append((nn(keyval), nn(name), nn(keycode), nn(group), nn(level)))
+ var keycodes = [];
+ var kc;
+ for(var keycode in CHARCODE_TO_NAME) {
+ kc = parseInt(keycode);
+ keycodes.push([kc, CHARCODE_TO_NAME[keycode], kc, 0, 0]);
+ }
+ //show("keycodes="+keycodes.toSource());
+ return keycodes;
+}
+
+XpraClient.prototype._get_desktop_size = function() {
+ return [this.container.clientWidth, this.container.clientHeight];
+}
+
+XpraClient.prototype._get_DPI = function() {
+ "use strict";
+ var dpi_div = document.getElementById("dpi");
+ if (dpi_div != undefined) {
+ //show("dpiX="+dpi_div.offsetWidth+", dpiY="+dpi_div.offsetHeight);
+ if (dpi_div.offsetWidth>0 && dpi_div.offsetHeight>0)
+ return Math.round((dpi_div.offsetWidth + dpi_div.offsetHeight) / 2.0);
+ }
+ //alternative:
+ if ('deviceXDPI' in screen)
+ return (screen.systemXDPI + screen.systemYDPI) / 2;
+ //default:
+ return 96;
+}
+
+XpraClient.prototype._get_screen_sizes = function() {
+ var dpi = this._get_DPI();
+ var screen_size = this._get_desktop_size();
+ var wmm = Math.round(screen_size[0]*25.4/dpi);
+ var hmm = Math.round(screen_size[1]*25.4/dpi);
+ var monitor = ["Canvas", 0, 0, screen_size[0], screen_size[1], wmm, hmm];
+ var screen = ["HTML", screen_size[0], screen_size[1],
+ wmm, hmm,
+ [monitor],
+ 0, 0, screen_size[0], screen_size[1]
+ ];
+ //just a single screen:
+ return [screen];
+}
+
+XpraClient.prototype._make_hello = function() {
+ return {
+ "version" : "0.15.0",
+ "platform" : this._guess_platform(),
+ "platform.name" : this._guess_platform_name(),
+ "platform.processor" : this._guess_platform_processor(),
+ "platform.platform" : navigator.appVersion,
+ "namespace" : true,
+ "client_type" : "HTML5",
+ "share" : false,
+ "auto_refresh_delay" : 500,
+ "randr_notify" : true,
+ "sound.server_driven" : true,
+ "generic_window_types" : true,
+ "server-window-resize" : true,
+ "notify-startup-complete" : true,
+ "generic-rgb-encodings" : true,
+ "window.raise" : true,
+ "encodings" : ["rgb"],
+ "raw_window_icons" : true,
+ //rgb24 is not efficient in HTML so don't use it:
+ //png and jpeg will need extra code
+ //"encodings.core" : ["rgb24", "rgb32", "png", "jpeg"],
+ "encodings.core" : ["rgb32"],
+ "encodings.rgb_formats" : this.RGB_FORMATS,
+ "encoding.generic" : true,
+ "encoding.transparency" : true,
+ "encoding.client_options" : true,
+ "encoding.csc_atoms" : true,
+ "encoding.uses_swscale" : false,
+ //video stuff we may handle later:
+ "encoding.video_reinit" : false,
+ "encoding.video_scaling" : false,
+ "encoding.csc_modes" : [],
+ //sound (not yet):
+ "sound.receive" : false,
+ "sound.send" : false,
+ //compression bits:
+ "zlib" : true,
+ "lz4" : false,
+ "compression_level" : 1,
+ "compressible_cursors" : true,
+ "encoding.rgb24zlib" : true,
+ "encoding.rgb_zlib" : true,
+ "encoding.rgb_lz4" : false,
+ "windows" : true,
+ //partial support:
+ "keyboard" : true,
+ "xkbmap_layout" : this._get_keyboard_layout(),
+ "xkbmap_keycodes" : this._get_keycodes(),
+ "desktop_size" : this._get_desktop_size(),
+ "screen_sizes" : this._get_screen_sizes(),
+ "dpi" : this._get_DPI(),
+ //not handled yet, but we will:
+ "clipboard_enabled" : false,
+ "notifications" : true,
+ "cursors" : true,
+ "bell" : true,
+ "system_tray" : true,
+ //we cannot handle this (GTK only):
+ "named_cursors" : false,
+ };
+}
+
+XpraClient.prototype._process_open = function(packet, ctx) {
+ console.log("sending hello");
+ var hello = ctx._make_hello();
+ ctx.protocol.send(["hello", hello]);
+}
+
+XpraClient.prototype._process_ping = function(packet, ctx) {
+ var echotime = packet[1];
+ var l1=0, l2=0, l3=0;
+ ctx.protocol.send(["ping_echo", echotime, l1, l2, l3, 0]);
+}
\ No newline at end of file
diff --git a/src/html5/include/xpra_protocol.js b/src/html5/include/xpra_protocol.js
new file mode 100644
index 0000000000..563d5974e7
--- /dev/null
+++ b/src/html5/include/xpra_protocol.js
@@ -0,0 +1,266 @@
+/*
+ * Copyright (c) 2013 Antoine Martin
+ * Copyright (c) 2014 Joshua Higgins
+ * Copyright (c) 2015 Spikes, Inc.
+ * Portions based on websock.js by Joel Martin
+ * Copyright (C) 2012 Joel Martin
+ *
+ * Licensed under MPL 2.0
+ *
+ * xpra wire protocol with worker support
+ *
+ * requires:
+ * bencode.js
+ * inflate.js
+ */
+
+
+/*
+A stub class to facilitate communication with the protocol when
+it is loaded in a worker
+*/
+function XpraProtocolWorkerHost() {
+ this.worker = null;
+ this.packet_handler = null;
+ this.packet_ctx = null;
+}
+
+XpraProtocolWorkerHost.prototype.open = function(uri) {
+ var me = this;
+ this.worker = new Worker('include/xpra_protocol.js');
+ this.worker.addEventListener('message', function(e) {
+ var data = e.data;
+ switch (data.c) {
+ case 'r':
+ me.worker.postMessage({'c': 'o', 'u': uri});
+ break;
+ case 'p':
+ if(me.packet_handler) {
+ me.packet_handler(data.p, me.packet_ctx);
+ }
+ break;
+ case 'l':
+ console.log(data.t);
+ break;
+ default:
+ console.error("got unknown command from worker");
+ console.error(e.data);
+ };
+ }, false);
+}
+
+XpraProtocolWorkerHost.prototype.close = function() {
+ this.worker.postMessage({'c': 'c'});
+}
+
+XpraProtocolWorkerHost.prototype.send = function(packet) {
+ this.worker.postMessage({'c': 's', 'p': packet});
+}
+
+XpraProtocolWorkerHost.prototype.set_packet_handler = function(callback, ctx) {
+ this.packet_handler = callback;
+ this.packet_ctx = ctx;
+}
+
+
+/*
+The main Xpra wire protocol
+*/
+function XpraProtocol() {
+ this.packet_handler = null;
+ this.packet_ctx = null;
+ this.websocket = null;
+ this.raw_packets = [];
+ this.mode = 'binary'; // Current WebSocket mode: 'binary', 'base64'
+ this.rQ = []; // Receive queue
+ this.rQi = 0; // Receive queue index
+ this.rQmax = 10000; // Max receive queue size before compacting
+ this.sQ = []; // Send queue
+}
+
+XpraProtocol.prototype.open = function(uri) {
+ var me = this;
+ // init
+ this.rQ = [];
+ this.rQi = 0;
+ this.sQ = [];
+ this.websocket = null;
+ // connect the socket
+ this.websocket = new WebSocket(uri, 'binary');
+ this.websocket.binaryType = 'arraybuffer';
+ this.websocket.onopen = function () {
+ me.packet_handler(['open'], me.packet_ctx);
+ };
+ this.websocket.onclose = function () {
+ me.packet_handler(['close'], me.packet_ctx);
+ };
+ this.websocket.onerror = function () {
+ me.packet_handler(['error'], me.packet_ctx);
+ };
+ this.websocket.onmessage = function (e) {
+ // push arraybuffer values onto the end
+ var u8 = new Uint8Array(e.data);
+ for (var i = 0; i < u8.length; i++) {
+ me.rQ.push(u8[i]);
+ }
+ // wait for 8 bytes
+ if (me.rQ.length > 8) {
+ me._process();
+ }
+ };
+}
+
+XpraProtocol.prototype.close = function() {
+ this.websocket.close();
+}
+
+XpraProtocol.prototype.send = function(packet) {
+ //debug("send worker:"+packet);
+ var bdata = bencode(packet);
+ //convert string to a byte array:
+ var cdata = [];
+ for (var i=0; i=0; i--)
+ header.push((len >> (8*i)) & 0xFF);
+ //concat data to header, saves an intermediate array which may or may not have
+ //been optimised out by the JS compiler anyway, but it's worth a shot
+ header = header.concat(cdata);
+ //debug("send("+packet+") "+data.byteLength+" bytes in packet for: "+bdata.substring(0, 32)+"..");
+ // put into buffer before send
+ this.websocket.send((new Uint8Array(header)).buffer);
+}
+
+XpraProtocol.prototype.set_packet_handler = function(callback, ctx) {
+ this.packet_handler = callback;
+ this.packet_ctx = ctx;
+}
+
+XpraProtocol.prototype._buffer_peek = function(bytes) {
+ return this.rQ.slice(0, 0+bytes);
+}
+
+XpraProtocol.prototype._buffer_shift = function(bytes) {
+ return this.rQ.splice(0, 0+bytes);;
+}
+
+XpraProtocol.prototype._process = function() {
+ // peek at first 8 bytes of buffer
+ var buf = this._buffer_peek(8);
+
+ if (buf[0]!=ord("P")) {
+ throw "invalid packet header format: " + buf[0];
+ }
+
+ var proto_flags = buf[1];
+ if (proto_flags!=0) {
+ throw "we cannot handle any protocol flags yet, sorry";
+ }
+ var level = buf[2];
+ var index = buf[3];
+ var packet_size = 0;
+ for (var i=0; i<4; i++) {
+ //debug("size header["+i+"]="+buf[4+i]);
+ packet_size = packet_size*0x100;
+ packet_size += buf[4+i];
+ }
+ //debug("packet_size="+packet_size+", level="+level+", index="+index);
+
+ // wait for packet to be complete
+ // the header is still on the buffer so wait for packetsize+headersize bytes!
+ if (this.rQ.length < packet_size+8) {
+ // we already shifted the header off the buffer?
+ debug("packet is not complete yet");
+ return;
+ }
+
+ // packet is complete but header is still on buffer
+ this._buffer_shift(8);
+ //debug("got a full packet, shifting off "+packet_size);
+ var packet_data = this._buffer_shift(packet_size);
+
+ //decompress it if needed:
+ if (level!=0) {
+ var inflated = new Zlib.Inflate(packet_data).decompress();
+ //debug("inflated("+packet_data+")="+inflated);
+ packet_data = inflated;
+ }
+
+ //save it for later? (partial raw packet)
+ if (index>0) {
+ //debug("added raw packet for index "+index);
+ this.raw_packets[index] = packet_data;
+ return;
+ }
+
+ //decode raw packet string into objects:
+ var packet = null;
+ try {
+ packet = bdecode(packet_data);
+ for (var index in this.raw_packets) {
+ packet[index] = this.raw_packets[index];
+ }
+ raw_packets = {}
+ // pass to our packet handler
+ this.packet_handler(packet, this.packet_ctx);
+ }
+ catch (e) {
+ console.error("error processing packet " + e)
+ //console.error("packet_data="+packet_data);
+ }
+
+ // see if buffer still has unread packets
+ if (this.rQ.length > 8) {
+ this._process();
+ }
+
+}
+
+
+/*
+If we are in a web worker, set up an instance of the protocol
+*/
+if (!(typeof window == "object" && typeof document == "object" && window.document === document)) {
+ // some required imports
+ // worker imports are relative to worker script path
+ importScripts('websock.js',
+ 'bencode.js',
+ 'inflate.min.js');
+ // make protocol instance
+ var protocol = new XpraProtocol();
+ // we create a custom packet handler which posts packet as a message
+ protocol.set_packet_handler(function (packet, ctx) {
+ postMessage({'c': 'p', 'p': packet});
+ }, null);
+ // attach listeners from main thread
+ self.addEventListener('message', function(e) {
+ var data = e.data;
+ switch (data.c) {
+ case 'o':
+ protocol.open(data.u);
+ break;
+ case 's':
+ protocol.send(data.p)
+ break;
+ case 'c':
+ // terminate the worker
+ protocol.close();
+ self.close();
+ break;
+ default:
+ postMessage({'c': 'l', 't': 'got unknown command from host'});
+ };
+ }, false);
+ // tell host we are ready
+ postMessage({'c': 'r'});
+}
\ No newline at end of file
diff --git a/src/html5/indexref.html b/src/html5/indexref.html
new file mode 100644
index 0000000000..c239943180
--- /dev/null
+++ b/src/html5/indexref.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+ xpra websockets client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+