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 + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + +