├── src ├── video.js ├── window.js ├── application.js ├── audio.js ├── layer.js └── xpra.js ├── icon.png ├── icon_color.png ├── .gitignore ├── package.json ├── server ├── main.php └── main.js ├── metadata.json ├── webpack.config.js ├── README.md ├── lib ├── aurora-xpra.js ├── bencode.js ├── MediaSourceUtil.js ├── Utilities.js ├── Protocol.js └── Keycodes.js ├── main.css ├── main.js └── LICENSE.md /src/video.js: -------------------------------------------------------------------------------- 1 | export default class VideoDecoder { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-xpra/HEAD/icon.png -------------------------------------------------------------------------------- /icon_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-xpra/HEAD/icon_color.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | *.swo 4 | .DS_Store 5 | .git-old 6 | *-tmp.* 7 | twistd.* 8 | .vagrant/* 9 | *.pyc 10 | *.bak 11 | *.o 12 | .tern-project 13 | sources 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osjs-xpra", 3 | "dependencies": { 4 | "av": "^0.4.9", 5 | "buffer": "^5.0.6", 6 | "coffee-loader": "^0.7.3", 7 | "lz4": "^0.5.3", 8 | "node-forge": "^0.7.1", 9 | "webpack": "^3.4.1", 10 | "zlibjs": "^0.3.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/main.php: -------------------------------------------------------------------------------- 1 | { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | const options = { 7 | exclude: /node_modules/ 8 | }; 9 | 10 | osjs.webpack.createPackageConfiguration(metadataFile, options).then((result) => { 11 | result.config.module.loaders.push({ 12 | test: /\.coffee$/, 13 | use: ['coffee-loader'] 14 | }); 15 | 16 | resolve(result.config); 17 | }).catch(reject); 18 | }); 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xpra client for OS.js (v2.1.0) 2 | 3 | This is a modified version of the Xpra HTML5 client that lets you run your Linux applications in OS.js. 4 | 5 | **THIS IS A VERY EARLY EXPERIMENT. MANY THINGS WILL CHANGE** 6 | 7 | [![YouTube Video](https://img.youtube.com/vi/c0safRR0ldM/0.jpg)](https://www.youtube.com/watch?v=c0safRR0ldM) 8 | 9 | ## Installation 10 | 11 | ``` 12 | ./bin/add-package.sh xpra Xpra https://github.com/os-js/OS.js-xpra.git 13 | ``` 14 | 15 | ## Usage 16 | 17 | You'll need: 18 | 19 | * Xpra installed (2.x or later) 20 | * Websockify (**with the python module**) 21 | * python-dbus, python-gobject 22 | 23 | At the moment you'll have to manually start a process, for example Firefox: 24 | 25 | ``` 26 | xpra --no-daemon --bind-tcp=127.0.0.1:10000 --start=firefox --html=on start :2 27 | ``` 28 | 29 | Sound support has been added, but might not work for all codecs. Please report to me if you have any issues or strange errors. 30 | 31 | ## Working 32 | 33 | * Window creation and events 34 | * Overlays (like menus, tooltips and general popupus) 35 | * Cursors and Icons 36 | * Mouse input 37 | * Keyboard input 38 | * Audio streaming 39 | 40 | ## TODO 41 | 42 | * SSL 43 | * Overlay keyboard events 44 | * Launcher via Service 45 | * Clipboard 46 | * Printer 47 | * Language Change 48 | * Macro handling from original source 49 | 50 | ## LICENSE 51 | 52 | See `LICENSE.md` as this package contains mixed licenses. 53 | -------------------------------------------------------------------------------- /lib/aurora-xpra.js: -------------------------------------------------------------------------------- 1 | // based on aurora-websocket.js https://github.com/fsbdev/aurora-websocket 2 | // MIT licensed 3 | 4 | (function() { 5 | var __hasProp = {}.hasOwnProperty, 6 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 7 | 8 | AV.XpraSource = (function(_super) { 9 | __extends(XpraSource, _super); 10 | 11 | function XpraSource() { 12 | // constructor 13 | } 14 | 15 | XpraSource.prototype.start = function() { 16 | return true; 17 | }; 18 | 19 | XpraSource.prototype.pause = function() { 20 | return true; 21 | }; 22 | 23 | XpraSource.prototype.reset = function() { 24 | return true; 25 | }; 26 | 27 | XpraSource.prototype._on_data = function(data) { 28 | var buf = new AV.Buffer(data); 29 | return this.emit('data', buf); 30 | }; 31 | 32 | return XpraSource; 33 | 34 | })(AV.EventEmitter); 35 | 36 | AV.Asset.fromXpraSource = function() { 37 | var source; 38 | source = new AV.XpraSource(); 39 | return new AV.Asset(source); 40 | }; 41 | 42 | AV.Player.fromXpraSource = function() { 43 | var asset; 44 | asset = AV.Asset.fromXpraSource(); 45 | return new AV.Player(asset); 46 | }; 47 | 48 | }).call(this); -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) 2011-2015, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationXpraWindow application-window-content { 32 | } 33 | 34 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | import ApplicationXpra from './src/application.js'; 32 | 33 | OSjs.Applications.ApplicationXpra = ApplicationXpra; 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Xpra HTML5 client 2 | 3 | * Copyright (c) 2013-2017 Antoine Martin 4 | * Copyright (c) 2014 Joshua Higgins 5 | * Copyright (c) 2015-2016 Spikes, Inc. 6 | * Licensed under MPL 2.0 7 | 8 | # Broadway 9 | 10 | Copyright (c) 2011, Project Authors (see AUTHORS file) 11 | All rights reserved. 12 | 13 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 14 | 15 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 16 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 17 | * Neither the names of the Project Authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | 21 | -- 22 | 23 | The 3-clause BSD above applies to all code except for code originating 24 | from the Android project (the .cpp files in Avc/). Those files are under 25 | the Android project's Apache 2.0 license. 26 | 27 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | /*eslint valid-jsdoc: "off"*/ 32 | (function() { 33 | 'use strict'; 34 | 35 | /* 36 | * See http://os.js.org/doc/tutorials/application-with-server-api.html 37 | */ 38 | 39 | /** 40 | * Registers your package when OS.js server starts. 41 | */ 42 | module.exports.register = function(env, metadata, servers) { 43 | }; 44 | 45 | /** 46 | * Registers your Application API methods 47 | */ 48 | module.exports.api = { 49 | test: function(env, http, resolve, reject, args) { 50 | resolve('This is a response from your application'); 51 | } 52 | }; 53 | 54 | })(); 55 | 56 | -------------------------------------------------------------------------------- /src/window.js: -------------------------------------------------------------------------------- 1 | import Layer from './layer.js'; 2 | 3 | const Window = OSjs.require('core/window'); 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // WINDOW 7 | /////////////////////////////////////////////////////////////////////////////// 8 | 9 | /** 10 | * A Xpra Window 11 | * 12 | * A normal OS.js window that is hooked with _on() 13 | */ 14 | export default class ApplicationXpraWindow extends Window { 15 | 16 | constructor(win, app, metadata) { 17 | super('ApplicationXpraWindow', { 18 | icon: metadata.icon, 19 | title: win.meta.title || metadata.name, 20 | minimized: win.meta.minimized === 1, 21 | maximized: win.meta.maximized === 1, 22 | fullscreen: win.meta.fullscreen === 1, 23 | width: win.w, 24 | height: win.h 25 | }, app); 26 | 27 | this.x = win.x; 28 | this.y = win.y; 29 | this.canvas = null; 30 | this.layer = new Layer(win.wid, win.props); 31 | this.wid = win.wid; 32 | this.overlays = {}; 33 | } 34 | 35 | destroy() { 36 | this.layer.destroy(); 37 | 38 | return super.destroy(...arguments); 39 | } 40 | 41 | /* 42 | * Handle movement packet 43 | */ 44 | move(x, y) { 45 | this.x = x; 46 | this.y = y; 47 | } 48 | 49 | /* 50 | * Initializes Window 51 | */ 52 | init(wmRef, app) { 53 | const root = super.init(...arguments); 54 | 55 | this.canvas = document.createElement('canvas'); 56 | root.appendChild(this.canvas); 57 | 58 | this.layer.init(this.canvas, this.getGeometry()); 59 | this.layer.updateCanvases(this.getGeometry()); 60 | 61 | return root; 62 | } 63 | 64 | /* 65 | * Removes an overlay 66 | */ 67 | removeOverlay(wid) { 68 | if ( this.overlays[wid] ) { 69 | delete this.overlays[wid]; 70 | } 71 | } 72 | 73 | /* 74 | * Sets up events 75 | */ 76 | addEvents(wid, canvas) { 77 | const topMargin = this._$top.offsetHeight; 78 | 79 | canvas.addEventListener('mousemove', (ev) => { 80 | this._app.client.processMouse(wid, ev, null, false, topMargin); 81 | }); 82 | canvas.addEventListener('mousedown', (ev) => { 83 | this._app.client.processMouse(wid, ev, true, false, topMargin); 84 | }); 85 | canvas.addEventListener('mouseup', (ev) => { 86 | this._app.client.processMouse(wid, ev, false, false, topMargin); 87 | }); 88 | canvas.addEventListener('wheel', (ev) => { 89 | this._app.client.processMouse(wid, ev, false, true, topMargin); 90 | }); 91 | canvas.addEventListener('mouseweheel', (ev) => { 92 | this._app.client.processMouse(wid, ev, false, true, topMargin); 93 | }); 94 | canvas.addEventListener('DOMMouseScroll', (ev) => { 95 | this._app.client.processMouse(wid, ev, false, true, topMargin); 96 | }); 97 | } 98 | 99 | /* 100 | * Sets up an overlay 101 | */ 102 | addOverlay(wid, layer, x, y, w, h) { 103 | if ( !this.canvas ) { 104 | console.warn('No canvas for', layer); 105 | return; 106 | } 107 | 108 | const geom = this.getGeometry(); 109 | const coverlay = document.createElement('canvas'); 110 | coverlay.setAttribute('data-wid', wid); 111 | coverlay.style.position = 'absolute'; 112 | coverlay.style.zIndex = 10; 113 | coverlay.style.left = String(x - geom.x) + 'px'; 114 | coverlay.style.top = String(y - geom.y) + 'px'; 115 | 116 | this._$root.appendChild(coverlay); 117 | 118 | layer.init(coverlay, {w, h}); 119 | 120 | this.addEvents(wid, coverlay); 121 | 122 | this.overlays[wid] = layer; 123 | } 124 | 125 | /* 126 | * Get client properties 127 | */ 128 | getClientProperties() { 129 | return this.layer.getClientProperties(); 130 | } 131 | 132 | /* 133 | * Get geometry 134 | */ 135 | getGeometry() { 136 | const {w, h} = this._dimension; 137 | const {x, y} = this._position; 138 | 139 | return {x, y, w, h}; 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /lib/bencode.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2009 Anton Ekblad 2 | * Copyright (c) 2013 Antoine Martin 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. */ 13 | 14 | /* 15 | * This is a modified version, suitable for xpra wire encoding: 16 | * - the input can be a string or byte array 17 | * - we do not sort lists or dictionaries (the existing order is preserved) 18 | * - error out instead of writing "null" and generating a broken stream 19 | * - handle booleans as ints (0, 1) 20 | */ 21 | 22 | 23 | // bencode an object 24 | export function bencode(obj) { 25 | if (obj === null || obj === undefined) { 26 | throw "invalid: cannot encode null"; 27 | } 28 | switch(btypeof(obj)) { 29 | case "string": return bstring(obj); 30 | case "number": return bint(obj); 31 | case "list": return blist(obj); 32 | case "dictionary": return bdict(obj); 33 | case "boolean": return bint(obj?1:0); 34 | default: throw "invalid object type in source: "+btypeof(obj); 35 | } 36 | } 37 | 38 | function uintToString(uintArray) { 39 | // apply in chunks of 10400 to avoid call stack overflow 40 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 41 | var s = ""; 42 | var skip = 10400; 43 | var slice = uintArray.slice; 44 | for (var i=0, len=uintArray.length; i 0) { 107 | p = bparse(str); 108 | if(null === p) { 109 | return null; 110 | } 111 | list[list.length] = p[0]; 112 | str = p[1]; 113 | } 114 | if(str.length <= 0) { 115 | throw "unexpected end of buffer reading list"; 116 | } 117 | return [list, str.substr(1)]; 118 | } 119 | 120 | // parse a bencoded dictionary 121 | function bparseDict(str) { 122 | var key, val, dict = {}; 123 | while(str.charAt(0) !== "e" && str.length > 0) { 124 | key = bparseString(str); 125 | if(null === key) { 126 | return; 127 | } 128 | val = bparse(key[1]); 129 | if(null === val) { 130 | return null; 131 | } 132 | dict[key[0]] = val[0]; 133 | str = val[1]; 134 | } 135 | if(str.length <= 0) { 136 | return null; 137 | } 138 | return [dict, str.substr(1)]; 139 | } 140 | 141 | // is the given string numeric? 142 | function isNum(str) { 143 | return !isNaN(str.toString()); 144 | } 145 | 146 | // returns the bencoding type of the given object 147 | function btypeof(obj) { 148 | var type = typeof obj; 149 | if(type === 'object') { 150 | if(typeof obj.length === 'undefined') { 151 | return "dictionary"; 152 | } 153 | return "list"; 154 | } 155 | return type; 156 | } 157 | 158 | // bencode a string 159 | function bstring(str) { 160 | return (str.length + ":" + str); 161 | } 162 | 163 | // bencode an integer 164 | function bint(num) { 165 | return "i" + num + "e"; 166 | } 167 | 168 | // bencode a list 169 | function blist(list) { 170 | var str; 171 | str = "l"; 172 | for(var key in list) { 173 | str += bencode(list[key]); 174 | } 175 | return str + "e"; 176 | } 177 | 178 | // bencode a dictionary 179 | function bdict(dict) { 180 | var str; 181 | str = "d"; 182 | for(var key in dict) { 183 | str += bencode(key) + bencode(dict[key]); 184 | } 185 | return str + "e"; 186 | } 187 | 188 | -------------------------------------------------------------------------------- /lib/MediaSourceUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Antoine Martin 3 | * Licensed under MPL 2.0 4 | * 5 | */ 6 | 'use strict'; 7 | 8 | import Utilities from './Utilities.js'; 9 | 10 | export var MediaSourceConstants = { 11 | 12 | CODEC_DESCRIPTION : { 13 | "mp4a" : 'mpeg4: aac', 14 | "aac+mpeg4" : 'mpeg4: aac', 15 | "mp3" : 'mp3', 16 | "mp3+mpeg4" : 'mpeg4: mp3', 17 | "wav" : 'wav', 18 | "wave" : 'wave', 19 | "flac" : 'flac', 20 | "opus" : 'opus', 21 | "vorbis" : 'vorbis', 22 | "opus+mka" : 'webm: opus', 23 | "opus+ogg" : 'ogg: opus', 24 | "vorbis+mka" : 'webm: vorbis', 25 | "vorbis+ogg" : 'ogg: vorbis', 26 | "speex+ogg" : 'ogg: speex', 27 | "flac+ogg" : 'ogg: flac', 28 | }, 29 | 30 | CODEC_STRING : { 31 | "aac+mpeg4" : 'audio/mp4; codecs="mp4a.40.2"', 32 | //"aac+mpeg4" : 'audio/mp4; codecs="aac51"', 33 | //"aac+mpeg4" : 'audio/aac', 34 | "mp3" : "audio/mpeg", 35 | "mp3+mpeg4" : 'audio/mp4; codecs="mp3"', 36 | //"mp3" : "audio/mp3", 37 | "ogg" : "audio/ogg", 38 | //"wave" : 'audio/wave', 39 | //"wav" : 'audio/wav; codec="1"', 40 | "wav" : 'audio/wav', 41 | "flac" : 'audio/flac', 42 | "opus+mka" : 'audio/webm; codecs="opus"', 43 | "vorbis+mka" : 'audio/webm; codecs="vorbis"', 44 | "vorbis+ogg" : 'audio/ogg; codecs="vorbis"', 45 | "speex+ogg" : 'audio/ogg; codecs="speex"', 46 | "flac+ogg" : 'audio/ogg; codecs="flac"', 47 | "opus+ogg" : 'audio/ogg; codecs="opus"', 48 | }, 49 | 50 | PREFERRED_CODEC_ORDER : [ 51 | "opus+mka", "vorbis+mka", 52 | "opus+ogg", "vorbis+ogg", 53 | "opus", "vorbis", 54 | "speex+ogg", "flac+ogg", 55 | "aac+mpeg4", "mp3+mpeg4", 56 | "mp3", "flac", "wav", "wave", 57 | ], 58 | 59 | H264_PROFILE_CODE : { 60 | //"baseline" : "42E0", 61 | "baseline" : "42C0", 62 | "main" : "4D40", 63 | "high" : "6400", 64 | "extended" : "58A0", 65 | }, 66 | 67 | H264_LEVEL_CODE : { 68 | "3.0" : "1E", 69 | "3.1" : "1F", 70 | "4.1" : "29", 71 | "5.1" : "33", 72 | }, 73 | 74 | READY_STATE : { 75 | 0 : "NOTHING", 76 | 1 : "METADATA", 77 | 2 : "CURRENT DATA", 78 | 3 : "FUTURE DATA", 79 | 4 : "ENOUGH DATA", 80 | }, 81 | 82 | NETWORK_STATE : { 83 | 0 : "EMPTY", 84 | 1 : "IDLE", 85 | 2 : "LOADING", 86 | 3 : "NO_SOURCE", 87 | }, 88 | 89 | ERROR_CODE : { 90 | 1 : "ABORTED: fetching process aborted by user", 91 | 2 : "NETWORK: error occurred when downloading", 92 | 3 : "DECODE: error occurred when decoding", 93 | 4 : "SRC_NOT_SUPPORTED", 94 | }, 95 | 96 | AURORA_CODECS : { 97 | "wav" : "lpcm", 98 | "mp3" : "mp3", 99 | "flac" : "flac", 100 | "aac+mpeg4" : "mp4a", 101 | } 102 | }; 103 | 104 | 105 | export var MediaSourceUtil = { 106 | 107 | getMediaSourceClass : function() { 108 | return window.MediaSource || window.WebKitMediaSource; 109 | }, 110 | 111 | getMediaSource : function() { 112 | var ms = MediaSourceUtil.getMediaSourceClass(); 113 | if(!ms) { 114 | throw Exception("no MediaSource support!"); 115 | } 116 | return new ms(); 117 | }, 118 | 119 | getAuroraAudioCodecs : function() { 120 | //IE is totally useless: 121 | if(Utilities.isIE()) { 122 | return {}; 123 | } 124 | var codecs_supported = {}; 125 | if(AV && AV.Decoder && AV.Decoder.find) { 126 | for (var codec_option in MediaSourceConstants.AURORA_CODECS) { 127 | var codec_string = MediaSourceConstants.AURORA_CODECS[codec_option]; 128 | var decoder = AV.Decoder.find(codec_string); 129 | if(decoder) { 130 | Utilities.log("audio codec aurora OK '"+codec_option+"' / '"+codec_string+"'"); 131 | codecs_supported[codec_option] = codec_string; 132 | } 133 | else { 134 | Utilities.log("audio codec aurora NOK '"+codec_option+"' / '"+codec_string+"'"); 135 | } 136 | } 137 | } 138 | return codecs_supported; 139 | }, 140 | 141 | getMediaSourceAudioCodecs : function(ignore_blacklist) { 142 | var media_source_class = MediaSourceUtil.getMediaSourceClass(); 143 | if(!media_source_class) { 144 | Utilities.log("audio forwarding: no media source API support"); 145 | return []; 146 | } 147 | //IE is totally useless: 148 | if(Utilities.isIE()) { 149 | return []; 150 | } 151 | var codecs_supported = []; 152 | for (var codec_option in MediaSourceConstants.CODEC_STRING) { 153 | var codec_string = MediaSourceConstants.CODEC_STRING[codec_option]; 154 | try { 155 | if(!media_source_class.isTypeSupported(codec_string)) { 156 | Utilities.log("audio codec MediaSource NOK: '"+codec_option+"' / '"+codec_string+"'"); 157 | //add whitelisting here? 158 | continue; 159 | } 160 | var blacklist = []; 161 | if (Utilities.isFirefox() || Utilities.isSafari()) { 162 | blacklist += ["opus+mka", "vorbis+mka"]; 163 | if (Utilities.isSafari()) { 164 | //this crashes Safari! 165 | blacklist += ["wav", ]; 166 | } 167 | } 168 | else if (Utilities.isChrome()) { 169 | blacklist = ["aac+mpeg4"]; 170 | } 171 | if(blacklist.indexOf(codec_option)>=0) { 172 | Utilities.log("audio codec MediaSource '"+codec_option+"' / '"+codec_string+"' is blacklisted for "+navigator.userAgent); 173 | if(ignore_blacklist) { 174 | Utilities.log("blacklist overruled!"); 175 | } 176 | else { 177 | continue; 178 | } 179 | } 180 | codecs_supported[codec_option] = codec_string; 181 | Utilities.log("audio codec MediaSource OK '"+codec_option+"' / '"+codec_string+"'"); 182 | } 183 | catch (e) { 184 | Utilities.error("audio error probing codec '"+codec_string+"' / '"+codec_string+"': "+e); 185 | } 186 | } 187 | Utilities.log("getMediaSourceAudioCodecs(", ignore_blacklist, ")=", codecs_supported); 188 | return codecs_supported; 189 | }, 190 | 191 | getSupportedAudioCodecs : function() { 192 | var codecs_supported = MediaSourceUtil.getMediaSourceAudioCodecs(); 193 | var aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs(); 194 | for (var codec_option in aurora_codecs) { 195 | if(codec_option in codecs_supported) { 196 | //we already have native MediaSource support! 197 | continue; 198 | } 199 | codecs_supported[codec_option] = aurora_codecs[codec_option]; 200 | } 201 | return codecs_supported; 202 | }, 203 | 204 | getDefaultAudioCodec : function(codecs) { 205 | if(!codecs) { 206 | return null; 207 | } 208 | var codec_options = Object.keys(codecs); 209 | for (var i = 0; i < MediaSourceConstants.PREFERRED_CODEC_ORDER.length; i++) { 210 | var codec_option = MediaSourceConstants.PREFERRED_CODEC_ORDER[i]; 211 | if(codec_options.indexOf(codec_option)>=0) { 212 | return codec_option; 213 | } 214 | } 215 | return Object.keys(codecs)[0]; 216 | }, 217 | 218 | addMediaSourceEventDebugListeners : function(media_source, source_type) { 219 | function debug_source_event(event) { 220 | var msg = ""+source_type+" source "+event; 221 | try { 222 | msg += ": "+media_source.readyState; 223 | } 224 | catch (e) { 225 | //don't care 226 | } 227 | console.debug(msg); 228 | } 229 | media_source.addEventListener('sourceopen', function(e) { debug_source_event('open'); }); 230 | media_source.addEventListener('sourceended', function(e) { debug_source_event('ended'); }); 231 | media_source.addEventListener('sourceclose', function(e) { debug_source_event('close'); }); 232 | media_source.addEventListener('error', function(e) { debug_source_event('error'); }); 233 | }, 234 | 235 | addMediaElementEventDebugListeners : function(media_element, element_type) { 236 | function debug_me_event(event) { 237 | console.debug(""+element_type+" "+event); 238 | } 239 | media_element.addEventListener('waiting', function() { debug_me_event("waiting"); }); 240 | media_element.addEventListener('stalled', function() { debug_me_event("stalled"); }); 241 | media_element.addEventListener('playing', function() { debug_me_event("playing"); }); 242 | media_element.addEventListener('loadstart', function() { debug_me_event("loadstart"); }); 243 | media_element.addEventListener('loadedmetadata', function() { debug_me_event("loadedmetadata"); }); 244 | media_element.addEventListener('loadeddata', function() { debug_me_event("loadeddata"); }); 245 | media_element.addEventListener('error', function() { debug_me_event("error"); }); 246 | media_element.addEventListener('canplay', function() { debug_me_event("canplay"); }); 247 | media_element.addEventListener('play', function() { debug_me_event("play"); }); 248 | }, 249 | 250 | addSourceBufferEventDebugListeners : function(source_buffer, element_type) { 251 | function debug_buffer_event(event) { 252 | var msg = ""+element_type+" buffer "+event; 253 | console.debug(msg); 254 | } 255 | asb.addEventListener('updatestart', function(e) { debug_buffer_event('updatestart'); }); 256 | asb.addEventListener('updateend', function(e) { debug_buffer_event('updateend'); }); 257 | asb.addEventListener('error', function(e) { debug_buffer_event('error'); }); 258 | asb.addEventListener('abort', function(e) { debug_buffer_event('abort'); }); 259 | }, 260 | } 261 | -------------------------------------------------------------------------------- /src/application.js: -------------------------------------------------------------------------------- 1 | import XpraClient from './xpra.js'; 2 | import ApplicationXpraWindow from './window.js'; 3 | import Layer from './layer.js'; 4 | 5 | const Application = OSjs.require('core/application'); 6 | const Window = OSjs.require('core/window'); 7 | const WindowManager = OSjs.require('core/window-manager'); 8 | const Dialog = OSjs.require('core/dialog'); 9 | const Menu = OSjs.require('gui/menu'); 10 | const Locales = OSjs.require('core/locales'); 11 | const Notification = OSjs.require('gui/notification'); 12 | 13 | /////////////////////////////////////////////////////////////////////////////// 14 | // APPLICATION 15 | /////////////////////////////////////////////////////////////////////////////// 16 | 17 | export default class ApplicationXpra extends Application { 18 | 19 | constructor(args, metadata) { 20 | super('ApplicationXpra', args, metadata, {}, { 21 | closeWithMain: false 22 | }); 23 | 24 | this.quitting = false; 25 | this.client = null; 26 | this.map = {}; 27 | } 28 | 29 | destroy() { 30 | if ( this.client ) { 31 | this.client = this.client.destroy(); 32 | } 33 | 34 | Notification.destroyIcon('Xpra'); 35 | 36 | return super.destroy(...arguments); 37 | } 38 | 39 | /* 40 | * Quit the application 41 | */ 42 | quit() { 43 | this.quitting = true; 44 | this.destroy(); 45 | } 46 | 47 | /* 48 | * Initialize Application 49 | */ 50 | init(settings, metadata) { 51 | super.init(...arguments); 52 | 53 | // Initialize client 54 | this.createClient(); 55 | this.createConnection(); 56 | 57 | // Hook into WindowManager 58 | const setDesktopSize = () => { 59 | const geom = WindowManager.instance.getWindowSpace(); 60 | this.client.setDesktopSize(geom); 61 | }; 62 | 63 | setDesktopSize(); 64 | 65 | WindowManager.instance._on('resize', setDesktopSize); 66 | 67 | // Make a notification icon 68 | const createMenu = (ev) => { 69 | Menu.create([{ 70 | title: this._getArgument('uri'), 71 | disabled: true 72 | }, { 73 | title: Locales._('LBL_EXIT'), 74 | onClick: () => this.quit() 75 | }, { 76 | title: Locales._('LBL_WINDOWS'), 77 | menu: this._getWindows().map((win) => { 78 | return { 79 | title: win._getTitle(), 80 | onClick: () => win._focus() 81 | }; 82 | }) 83 | }], ev); 84 | }; 85 | 86 | Notification.createIcon('Xpra', { 87 | icon: this._getResource('icon_color.png'), 88 | onClick: createMenu, 89 | onContextMenu: createMenu 90 | }); 91 | } 92 | 93 | /* 94 | * Creates a new connection 95 | */ 96 | createConnection() { 97 | if ( !this.client ) { 98 | return; 99 | } 100 | 101 | const connect = (uri) => { 102 | this.client.connect(uri); 103 | this._setArgument('uri', uri); 104 | }; 105 | 106 | let uri = this._getArgument('uri'); 107 | if ( uri ) { 108 | connect(uri); 109 | } else { 110 | Dialog.create('Input', { 111 | title: 'Xpra Connection Dialog', 112 | message: 'Enter the server address to connect to', 113 | value: 'ws://localhost:10000' 114 | }, (ev, btn, value) => { 115 | if ( btn === 'ok' && value ) { 116 | connect(value); 117 | } else { 118 | this.destroy(); 119 | } 120 | }); 121 | } 122 | } 123 | 124 | /* 125 | * Creates a new Xpra client 126 | */ 127 | createClient() { 128 | if ( this.client ) { 129 | return; 130 | } 131 | 132 | const findOverlay = (wid) => this.map[wid]; 133 | 134 | const createWindow = (wid, x, y, w, h, meta, props) => { 135 | console.info('XpraApplication', 'Creating window', [wid, x, y, w, h, meta, props]); 136 | const win = new ApplicationXpraWindow({ 137 | wid: wid, 138 | w: w, 139 | h: h, 140 | x: x, 141 | y: y, 142 | props: props, 143 | meta: meta 144 | }, this, this.__metadata); 145 | 146 | win._on('inited', () => { 147 | const geom = win.getGeometry(); 148 | const props = win.getClientProperties(); 149 | 150 | win.addEvents(wid, win.canvas); 151 | 152 | this.client.send(['map-window', wid, geom.x, geom.y, geom.w, geom.h, props]); 153 | 154 | win._focus(); 155 | }); 156 | 157 | win._on('keydown', (ev, code) => { 158 | return this.client.processKey(wid, true, ev, code); 159 | }); 160 | 161 | win._on('keyup', (ev, code) => { 162 | return this.client.processKey(wid, false, ev, code); 163 | }); 164 | 165 | win._on('keypress', (ev, code, shiftKey, ctrlKey, altKey) => { 166 | return this.client.processKeyPress(wid, ev, code); 167 | }); 168 | 169 | win._on('resized, moved, maximize, restore', () => { 170 | const geom = win.getGeometry(); 171 | const props = win.getClientProperties(); 172 | 173 | win.layer.updateCanvases(geom); 174 | 175 | this.client.send(['configure-window', wid, geom.x, geom.y, geom.w, geom.h, props]); 176 | }); 177 | 178 | win._on('destroy', () => { 179 | if ( !this.quitting ) { 180 | this.client.send(['close-window', wid]); 181 | } 182 | }); 183 | 184 | win._on('focus', () => { 185 | this.client.send(['focus', wid]); 186 | }); 187 | 188 | return this._addWindow(win); 189 | }; 190 | 191 | this.client = new XpraClient(); 192 | 193 | this.client.on('disconnect', () => { 194 | Notification.create({ 195 | icon: this._getResource('icon_color.png'), 196 | title: 'Disconnected from Xpra', 197 | message: this._getArgument('uri') 198 | }); 199 | Object.keys(this.map).forEach((w) => this.map[w].destroy()); 200 | }); 201 | 202 | this.client.on('connect', () => { 203 | Notification.create({ 204 | icon: this._getResource('icon_color.png'), 205 | title: 'Connected to Xpra', 206 | message: this._getArgument('uri') 207 | }); 208 | }); 209 | 210 | this.client.on('window-metadata', (wid, meta) => { 211 | const found = findOverlay(wid); 212 | if ( found instanceof Window ) { 213 | if ( meta.title ) { 214 | found._setTitle(meta.title); 215 | } 216 | } 217 | }); 218 | 219 | this.client.on('window-icon', (wid, w, h, encoding, data) => { 220 | const found = findOverlay(wid); 221 | if ( found instanceof Window ) { 222 | if ( encoding === 'png' ) { 223 | const src = 'data:image/' + encoding + ';base64,' + found.layer.arrayBufferToBase64(data); 224 | found._setIcon(src); 225 | } 226 | } 227 | }); 228 | 229 | this.client.on('window-move-resize', (wid, x, y, w, h) => { 230 | const found = findOverlay(wid); 231 | if ( found instanceof Window ) { 232 | found._resize(w, h); 233 | found.move(x, y); 234 | } 235 | }); 236 | 237 | this.client.on('window-resized', (wid, w, h) => { 238 | const found = findOverlay(wid); 239 | if ( found instanceof Window ) { 240 | found._resize(w, h); 241 | } 242 | }); 243 | 244 | this.client.on('raise-window', (wid) => { 245 | const found = findOverlay(wid); 246 | if ( found instanceof Window ) { 247 | found._focus(); 248 | } 249 | }); 250 | 251 | this.client.on('lost-window', (wid) => { 252 | const found = this.map[wid]; 253 | if ( found ) { 254 | if ( found instanceof Window ) { 255 | found.removeOverlay(wid); 256 | } 257 | found.destroy(); 258 | 259 | delete this.map[wid]; 260 | } 261 | }); 262 | 263 | this.client.on('configure-override-redirect', (wid, x, y, w, h, meta, props) => { 264 | const redirect = this.map[wid]; 265 | if ( redirect ) { 266 | redirect.updateCanvases({w, h}); 267 | } 268 | }); 269 | 270 | this.client.on('new-override-redirect', (wid, x, y, w, h, meta, props) => { 271 | if ( this.map[wid] ) { 272 | return; 273 | } 274 | 275 | const parentWid = meta['transient-for']; 276 | const pwin = this.map[parentWid]; 277 | if ( pwin instanceof Window ) { 278 | this.map[wid] = new Layer(wid, props); 279 | pwin.addOverlay(wid, this.map[wid], x, y, w, h); 280 | } else { 281 | console.warn('TODO'); 282 | } 283 | }); 284 | 285 | this.client.on('new-window', (wid, x, y, w, h, meta, props) => { 286 | this.map[wid] = createWindow(wid, x, y, w, h, meta, props); 287 | }); 288 | 289 | this.client.on('redraw', (wid) => { 290 | const found = findOverlay(wid); 291 | if ( found ) { 292 | const instance = found instanceof Window ? found.layer : found; 293 | instance.draw(); 294 | } 295 | }); 296 | 297 | this.client.on('cursor', (encoding, w, h, xhot, yhot, img_data) => { 298 | this._getWindows().forEach((w) => { 299 | w.layer.setCursor(encoding, w, h, xhot, yhot, img_data); 300 | }); 301 | }); 302 | 303 | this.client.on('reset-cursor', () => { 304 | this._getWindows().forEach((w) => { 305 | w.layer.setCursor(); 306 | }); 307 | }); 308 | 309 | this.client.on('paint', (wid, x, y, width, height, coding, data, packet_sequence, rowstride, options, cb) => { 310 | const found = findOverlay(wid); 311 | if ( found ) { 312 | const instance = found instanceof Window ? found.layer : found; 313 | instance.paint(x, y, width, height, coding, data, packet_sequence, rowstride, options, cb); 314 | } 315 | }); 316 | } 317 | } 318 | 319 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | import Utilities from '../lib/Utilities.js'; 2 | import {MediaSourceUtil, MediaSourceConstants} from '../lib/MediaSourceUtil.js'; 3 | 4 | import AV from 'av'; 5 | 6 | window.AV = AV; 7 | window.AVXpra = require('../lib/aurora-xpra.js'); 8 | 9 | const EventHandler = OSjs.require('helpers/event-handler'); 10 | 11 | let MIN_START_BUFFERS = 4; 12 | let MAX_BUFFERS = 250; 13 | 14 | export default class AudioDecoder extends EventHandler { 15 | 16 | constructor(uuid) { 17 | super(); 18 | 19 | this.uri = ''; 20 | this.uuid = uuid; 21 | this.enabled = true; 22 | this.server_codecs = {}; 23 | this.available_codecs = {}; 24 | this.framework = null; 25 | this.codec = null; 26 | this.context = null; 27 | this.audio_buffers = []; 28 | this.audio_source_ready = false; 29 | this.audio_buffers_count = 0; 30 | this.audio_aurora_ctx = null; 31 | this.media_source = null; 32 | 33 | this.sources = { 34 | mediasource: !!MediaSourceUtil.getMediaSourceClass() && !!AV.Player.fromXpraSource, 35 | aurora: !!AV.Player.fromXpraSource, 36 | httpstream: true 37 | }; 38 | 39 | this.codecs = { 40 | aurora: {}, 41 | mediasource: {}, 42 | httpstream: ['mp3'] 43 | }; 44 | } 45 | 46 | destroy() { 47 | if ( !this.protocol ) { 48 | return; 49 | } 50 | 51 | this.emit('destroy'); 52 | 53 | if ( this.media_source ) { 54 | try { 55 | if ( this.audio_source_buffer ) { 56 | this.media_source.removeSourceBuffer(this.audio_source_buffer); 57 | this.audio_source_buffer = null; 58 | } 59 | if ( this.media_source.readyState === 'open' ) { 60 | this.media_source.endOfStream(); 61 | } 62 | } catch ( e ) {} 63 | } 64 | 65 | if ( this.$audio ) { 66 | this.$audio.src = ''; 67 | this.$audio.load(); 68 | try { 69 | this.$audio.parentNode.removeChild(this.$audio); 70 | } catch (e) {} 71 | } 72 | 73 | this.audio_aurora_ctx = null; 74 | this.media_source = null; 75 | } 76 | 77 | init(uri) { 78 | if ( !this.enabled ) { 79 | return; 80 | } 81 | 82 | this.uri = uri; 83 | 84 | console.group('XpraClient', 'AudioDecoder::init()'); 85 | 86 | this.$audio = document.createElement('audio'); 87 | this.$audio.setAttribute('autoplay', true); 88 | this.$audio.setAttribute('controls', false); 89 | this.$audio.setAttribute('loop', true); 90 | this.$audio.style.position = 'absolute'; 91 | this.$audio.style.top = '-10000px'; 92 | this.$audio.style.left = '-10000px'; 93 | this.$audio.style.width = '1px'; 94 | this.$audio.style.height = '1px'; 95 | this.$audio.addEventListener('error', (e) => { 96 | console.error('audio source error', e); 97 | }); 98 | this.$audio.addEventListener('play', (e) => { 99 | console.warn('audio source started playing', e); 100 | }); 101 | document.body.appendChild(this.$audio); 102 | 103 | this.context = Utilities.getAudioContext(); 104 | 105 | if ( this.sources.mediasource ) { 106 | this.codecs.mediasource = MediaSourceUtil.getMediaSourceAudioCodecs([]); 107 | 108 | console.debug('Checking "mediasource"', this.codecs.mediasource); 109 | Object.keys(this.codecs.mediasource).forEach((n) => { 110 | this.available_codecs[n] = this.codecs.mediasource[n]; 111 | }); 112 | } 113 | 114 | if ( this.sources.aurora ) { 115 | this.codecs.aurora = MediaSourceUtil.getAuroraAudioCodecs(); 116 | 117 | console.debug('Checking "aurora"', this.codecs.aurora); 118 | Object.keys(this.codecs.aurora).forEach((n) => { 119 | if ( !(n in this.available_codecs) ) { 120 | this.available_codecs[n] = this.codecs.aurora[n]; 121 | } 122 | }); 123 | } 124 | 125 | if ( this.sources.httpstream ) { 126 | console.debug('Checking "httpstream"', this.codecs.httpstream); 127 | this.codecs.httpstream.forEach((n) => { 128 | this.available_codecs[n] = n; 129 | }); 130 | } 131 | 132 | if ( !Object.keys(this.available_codecs).length ) { 133 | console.warn('No audio codecs found'); 134 | this.enabled = false; 135 | this.codec = null; 136 | } 137 | 138 | console.debug('Found audio codecs', this.available_codecs); 139 | 140 | if ( !(this.codec in this.available_codecs) ) { 141 | const defaultCodec = MediaSourceUtil.getDefaultAudioCodec(this.available_codecs); 142 | console.debug('No codec was found, trying', defaultCodec); 143 | this.codec = defaultCodec; 144 | 145 | if ( this.codec ) { 146 | if ( this.sources.mediasource && (this.codec in this.codecs.mediasource) ) { 147 | this.framework = 'mediasource'; 148 | } else if ( this.sources.aurora && !Utilities.isIE() ) { 149 | this.framework = 'aurora'; 150 | } else if ( this.sources.httpstream ) { 151 | this.framework = 'http-stream'; 152 | } else { 153 | console.warn('Did not find any framework...'); 154 | } 155 | } 156 | } 157 | 158 | console.info('Codecs', this.available_codecs, 'from', this.codecs); 159 | console.info('Using audio framework', this.framework, this.codec); 160 | 161 | console.groupEnd(); 162 | } 163 | 164 | setup(hello) { 165 | console.group('XpraClient', 'AudioDecoder::setup()'); 166 | 167 | if ( hello['sound.send'] ) { 168 | this.server_codecs = hello['sound.encoders'] || []; 169 | if ( !this.server_codecs.length ) { 170 | this.enabled = false; 171 | } else { 172 | console.debug('Server is sending audio with', this.server_codecs); 173 | 174 | if ( this.server_codecs.indexOf(this.codec) === -1 ) { 175 | const pref = MediaSourceConstants.PREFERRED_CODEC_ORDER; 176 | const found = pref.find((p) => { 177 | console.debug('Trying', p, '...'); 178 | if ( (p in this.available_codecs) && this.server_codecs.indexOf(p) !== -1 ) { 179 | return true; 180 | } 181 | return false; 182 | }); 183 | 184 | if ( found ) { 185 | if ( this.codecs.mediasource[found] ) { 186 | this.codec = 'mediasource'; 187 | } else { 188 | this.codec = 'aurora'; 189 | } 190 | } else { 191 | this.codec = null; 192 | console.debug('... but it looks like our codec was not supported'); 193 | } 194 | } 195 | } 196 | } else { 197 | this.enabled = false; 198 | } 199 | 200 | if ( !this.codec ) { 201 | this.enabled = false; 202 | } 203 | 204 | if ( this.enabled ) { 205 | console.info('We\'re using audio with', this.framework, this.codec); 206 | 207 | if ( this.framework === 'http-stream' ) { 208 | this.$audio.src = this.uri.replace(/^ws/, 'http') + '/audio.mp3?uuid=' + this.uuid; 209 | console.info('Streaming audio from', this.$audio.src); 210 | } else if ( this.framework === 'mediasource' ) { 211 | this.media_source = MediaSourceUtil.getMediaSource(); 212 | this.$audio.src = window.URL.createObjectURL(this.media_source); 213 | console.info('Starting streaming audio from', this.$audio.src); 214 | 215 | this.media_source.addEventListener('sourceopen', (e) => { 216 | console.error(e); 217 | this.destroy(); 218 | }); 219 | 220 | this.media_source.addEventListener('sourceopen', (e) => { 221 | console.warn('audio source was opened', e); 222 | 223 | if ( this.audio_source_ready ) { 224 | return; 225 | } 226 | 227 | const codec_string = MediaSourceConstants.CODEC_STRING[this.codec]; 228 | if ( !codec_string ) { 229 | this.destroy(); 230 | return; 231 | } 232 | 233 | try { 234 | this.audio_source_buffer = this.media_source.addSourceBuffer(codec_string); 235 | } catch (e) { 236 | this.destroy(); 237 | return; 238 | } 239 | 240 | this.audio_source_buffer.mode = 'sequence'; 241 | 242 | this.audio_source_buffer.addEventListener('error', (e) => { 243 | console.error(e); 244 | }); 245 | 246 | this.emit('ready', [this.codec]); 247 | this.audio_source_ready = true; 248 | }); 249 | } else { 250 | this.audio_aurora_ctx = AV.Player.fromXpraSource(); 251 | this.emit('ready', [this.codec]); 252 | console.info('Starting streaming audio from', this.audio_aurora_ctx); 253 | } 254 | } else { 255 | console.warn('Could not enable audio'); 256 | } 257 | 258 | console.groupEnd(); 259 | } 260 | 261 | play() { 262 | console.info('GOT SIGNAL TO START PLAYING'); 263 | if ( this.framework === 'mediasource' ) { 264 | this.$audio.play(); 265 | } else { 266 | this.audio_aurora_ctx.play(); 267 | } 268 | } 269 | 270 | handle(codec, buf, options, metadata) { 271 | const isReady = () => { 272 | if ( this.framework === 'mediasource' ) { 273 | const asb = this.audio_source_buffer; 274 | return !!asb && !asb.updating; 275 | } 276 | 277 | return !!this.audio_aurora_ctx; 278 | }; 279 | 280 | if ( codec !== this.codec ) { 281 | return; 282 | } 283 | 284 | if ( options['start-of-stream'] === 1 ) { 285 | this.play(); 286 | return; 287 | } 288 | 289 | if ( options['end-of-stream'] === 1 ) { 290 | this.destroy(); 291 | } 292 | 293 | if ( this.audio_buffers.length >= MAX_BUFFERS ) { 294 | this.destroy(); 295 | return; 296 | } 297 | 298 | let i, j, v; 299 | if ( metadata ) { 300 | for ( i = 0; i < metadata.length; i++ ) { 301 | /* eslint new-cap: "off" */ 302 | this.audio_buffers.push(Utilities.StringToUint8(metadata[i])); 303 | } 304 | MIN_START_BUFFERS = 1; 305 | } 306 | 307 | if ( buf ) { 308 | this.audio_buffers.push(buf); 309 | } 310 | 311 | var ab = this.audio_buffers; 312 | if ( isReady() && (this.audio_buffers_count > 0 || ab.length >= MIN_START_BUFFERS) ) { 313 | if ( ab.length === 1 ) { 314 | buf = ab[0]; 315 | } else { 316 | var size = 0; 317 | for ( i = 0, j = ab.length; i < j; ++i ) { 318 | size += ab[i].length; 319 | } 320 | 321 | buf = new Uint8Array(size); 322 | size = 0; 323 | for ( i = 0, j = ab.length; i < j; ++i ) { 324 | v = ab[i]; 325 | if ( v.length > 0 ) { 326 | buf.set(v, size); 327 | size += v.length; 328 | } 329 | } 330 | } 331 | 332 | this.audio_buffers_count += 1; 333 | this.audio_buffers = []; 334 | if ( this.framework === 'mediasource' ) { 335 | this.audio_source_buffer.appendBuffer(buf); 336 | } else { 337 | this.audio_aurora_ctx.asset.source._on_data(buf); 338 | } 339 | } 340 | } 341 | 342 | getAvailableCodecs() { 343 | return this.available_codecs; 344 | } 345 | 346 | } 347 | -------------------------------------------------------------------------------- /lib/Utilities.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Xpra. 3 | * Copyright (C) 2016 Antoine Martin 4 | * Copyright (c) 2016 Spikes, Inc. 5 | * Licensed under MPL 2.1, see: 6 | * http://www.mozilla.org/MPL/2.1/ 7 | * 8 | */ 9 | 10 | 'use strict'; 11 | 12 | var Utilities = { 13 | VERSION : "2.2", 14 | 15 | error : function() { 16 | console.error.apply(console, arguments); 17 | }, 18 | warn : function() { 19 | console.log.apply(console, arguments); 20 | }, 21 | log : function() { 22 | console.log.apply(console, arguments); 23 | }, 24 | 25 | getHexUUID: function() { 26 | var s = []; 27 | var hexDigits = "0123456789abcdef"; 28 | for (var i = 0; i < 36; i++) { 29 | if (i==8 || i==13 || i==18 || i==23) { 30 | s[i] = "-"; 31 | } 32 | else { 33 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 34 | } 35 | } 36 | var uuid = s.join(""); 37 | return uuid; 38 | }, 39 | 40 | getSalt: function(l) { 41 | if(l<32 || l>256) { 42 | throw 'invalid salt length'; 43 | } 44 | var s = ''; 45 | while (s.length= 0; 186 | }, 187 | isOpera : function() { 188 | var ua = navigator.userAgent.toLowerCase(); 189 | return ua.indexOf("opera") >= 0; 190 | }, 191 | isSafari : function() { 192 | var ua = navigator.userAgent.toLowerCase(); 193 | return ua.indexOf("safari") >= 0 && ua.indexOf('chrome') < 0; 194 | }, 195 | isChrome : function () { 196 | var isChromium = window.chrome, 197 | winNav = window.navigator, 198 | vendorName = winNav.vendor, 199 | isOpera = winNav.userAgent.indexOf("OPR") > -1, 200 | isIEedge = winNav.userAgent.indexOf("Edge") > -1, 201 | isIOSChrome = winNav.userAgent.match("CriOS"); 202 | if (isIOSChrome) { 203 | return true; 204 | } 205 | else if (isChromium !== null && isChromium !== undefined && vendorName === "Google Inc." && isOpera == false && isIEedge == false) { 206 | return true; 207 | } 208 | else { 209 | return false; 210 | } 211 | }, 212 | isIE : function() { 213 | return navigator.userAgent.indexOf("MSIE") != -1; 214 | }, 215 | 216 | getSimpleUserAgentString : function() { 217 | var ua = navigator.userAgent.toLowerCase(); 218 | if (Utilities.isFirefox()) { 219 | return "Firefox"; 220 | } 221 | else if (Utilities.isOpera()) { 222 | return "Opera"; 223 | } 224 | else if (Utilities.isSafari()) { 225 | return "Safari"; 226 | } 227 | else if (Utilities.isChrome()) { 228 | return "Chrome"; 229 | } 230 | else if (Utilities.isIE()) { 231 | return "MSIE"; 232 | } 233 | else { 234 | return ""; 235 | } 236 | }, 237 | 238 | getColorGamut : function() { 239 | if (!window.matchMedia) { 240 | //unknown 241 | return ""; 242 | } 243 | else if (window.matchMedia('(color-gamut: rec2020)').matches) { 244 | return "rec2020"; 245 | } 246 | else if (window.matchMedia('(color-gamut: p3)').matches) { 247 | return "P3"; 248 | } 249 | else if (window.matchMedia('(color-gamut: srgb)').matches) { 250 | return "srgb"; 251 | } 252 | else { 253 | return ""; 254 | } 255 | }, 256 | 257 | isEventSupported : function(event) { 258 | var testEl = document.createElement('div'); 259 | var isSupported; 260 | 261 | event = 'on' + event; 262 | isSupported = (event in testEl); 263 | 264 | if (!isSupported) { 265 | testEl.setAttribute(event, 'return;'); 266 | isSupported = typeof testEl[event] === 'function'; 267 | } 268 | testEl = null; 269 | return isSupported; 270 | }, 271 | 272 | //https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js 273 | //BSD license 274 | normalizeWheel : function(/*object*/ event) /*object*/ { 275 | // Reasonable defaults 276 | var PIXEL_STEP = 10; 277 | var LINE_HEIGHT = 40; 278 | var PAGE_HEIGHT = 800; 279 | 280 | var sX = 0, sY = 0, // spinX, spinY 281 | pX = 0, pY = 0; // pixelX, pixelY 282 | 283 | // Legacy 284 | if ('detail' in event) { sY = event.detail; } 285 | if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; } 286 | if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; } 287 | if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; } 288 | 289 | // side scrolling on FF with DOMMouseScroll 290 | if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) { 291 | sX = sY; 292 | sY = 0; 293 | } 294 | 295 | pX = sX * PIXEL_STEP; 296 | pY = sY * PIXEL_STEP; 297 | 298 | if ('deltaY' in event) { pY = event.deltaY; } 299 | if ('deltaX' in event) { pX = event.deltaX; } 300 | 301 | if ((pX || pY) && event.deltaMode) { 302 | if (event.deltaMode == 1) { // delta in LINE units 303 | pX *= LINE_HEIGHT; 304 | pY *= LINE_HEIGHT; 305 | } else { // delta in PAGE units 306 | pX *= PAGE_HEIGHT; 307 | pY *= PAGE_HEIGHT; 308 | } 309 | } 310 | 311 | // Fall-back if spin cannot be determined 312 | if (pX && !sX) { sX = (pX < 1) ? -1 : 1; } 313 | if (pY && !sY) { sY = (pY < 1) ? -1 : 1; } 314 | 315 | return { 316 | spinX : sX, 317 | spinY : sY, 318 | pixelX : pX, 319 | pixelY : pY, 320 | deltaMode : (event.deltaMode || 0), 321 | }; 322 | }, 323 | 324 | saveFile : function(filename, data, mimetype) { 325 | var a = document.createElement("a"); 326 | a.style = "display: none"; 327 | document.body.appendChild(a); 328 | var blob = new Blob([data], mimetype); 329 | var url = window.URL.createObjectURL(blob); 330 | a.href = url; 331 | a.download = filename; 332 | a.click(); 333 | window.URL.revokeObjectURL(url); 334 | }, 335 | 336 | //IE is retarded: 337 | endsWith : function (str, suffix) { 338 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 339 | }, 340 | 341 | monotonicTime : function() { 342 | if (performance) { 343 | return performance.now()*1000.0; 344 | } 345 | return Date().now(); 346 | }, 347 | 348 | StringToUint8 : function(str) { 349 | var u8a = new Uint8Array(str.length); 350 | for(var i=0,j=str.length;i 0) { 383 | var key = headerPair.substring(0, index); 384 | var val = headerPair.substring(index + 2); 385 | headers[key] = val; 386 | } 387 | } 388 | return headers; 389 | } 390 | }; 391 | 392 | 393 | var MOVERESIZE_SIZE_TOPLEFT = 0; 394 | var MOVERESIZE_SIZE_TOP = 1; 395 | var MOVERESIZE_SIZE_TOPRIGHT = 2; 396 | var MOVERESIZE_SIZE_RIGHT = 3; 397 | var MOVERESIZE_SIZE_BOTTOMRIGHT = 4; 398 | var MOVERESIZE_SIZE_BOTTOM = 5; 399 | var MOVERESIZE_SIZE_BOTTOMLEFT = 6; 400 | var MOVERESIZE_SIZE_LEFT = 7; 401 | var MOVERESIZE_MOVE = 8; 402 | var MOVERESIZE_SIZE_KEYBOARD = 9; 403 | var MOVERESIZE_MOVE_KEYBOARD = 10; 404 | var MOVERESIZE_CANCEL = 11; 405 | var MOVERESIZE_DIRECTION_STRING = { 406 | 0 : "SIZE_TOPLEFT", 407 | 1 : "SIZE_TOP", 408 | 2 : "SIZE_TOPRIGHT", 409 | 3 : "SIZE_RIGHT", 410 | 4 : "SIZE_BOTTOMRIGHT", 411 | 5 : "SIZE_BOTTOM", 412 | 6 : "SIZE_BOTTOMLEFT", 413 | 7 : "SIZE_LEFT", 414 | 8 : "MOVE", 415 | 9 : "SIZE_KEYBOARD", 416 | 10 : "MOVE_KEYBOARD", 417 | 11 : "CANCEL", 418 | }; 419 | var MOVERESIZE_DIRECTION_JS_NAME = { 420 | 0 : "nw", 421 | 1 : "n", 422 | 2 : "ne", 423 | 3 : "e", 424 | 4 : "se", 425 | 5 : "s", 426 | 6 : "sw", 427 | 7 : "w", 428 | }; 429 | 430 | export default Utilities; 431 | -------------------------------------------------------------------------------- /lib/Protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013-2017 Antoine Martin 3 | * Copyright (c) 2016 David Brushinski 4 | * Copyright (c) 2014 Joshua Higgins 5 | * Copyright (c) 2015 Spikes, Inc. 6 | * Portions based on websock.js by Joel Martin 7 | * Copyright (C) 2012 Joel Martin 8 | * 9 | * Licensed under MPL 2.0 10 | * 11 | * xpra wire protocol with worker support 12 | * 13 | * requires: 14 | * bencode.js 15 | * inflate.js 16 | * lz4.js 17 | */ 18 | //var bencode = require('bencode'); 19 | import {bencode, bdecode, ord} from './bencode.js'; 20 | import LZ4 from 'lz4'; 21 | import Zlib from 'zlibjs'; 22 | import forge from 'node-forge'; 23 | 24 | /* 25 | A stub class to facilitate communication with the protocol when 26 | it is loaded in a worker 27 | */ 28 | function XpraProtocolWorkerHost() { 29 | this.worker = null; 30 | this.packet_handler = null; 31 | this.packet_ctx = null; 32 | } 33 | 34 | XpraProtocolWorkerHost.prototype.open = function(uri) { 35 | var me = this; 36 | if (this.worker) { 37 | //re-use the existing worker: 38 | this.worker.postMessage({'c': 'o', 'u': uri}); 39 | return; 40 | } 41 | this.worker = new Worker('js/Protocol.js'); 42 | this.worker.addEventListener('message', function(e) { 43 | var data = e.data; 44 | switch (data.c) { 45 | case 'r': 46 | me.worker.postMessage({'c': 'o', 'u': uri}); 47 | break; 48 | case 'p': 49 | if(me.packet_handler) { 50 | me.packet_handler(data.p, me.packet_ctx); 51 | } 52 | break; 53 | case 'l': 54 | console.log(data.t); 55 | break; 56 | default: 57 | console.error("got unknown command from worker"); 58 | console.error(e.data); 59 | }; 60 | }, false); 61 | } 62 | 63 | XpraProtocolWorkerHost.prototype.close = function() { 64 | this.worker.postMessage({'c': 'c'}); 65 | } 66 | 67 | XpraProtocolWorkerHost.prototype.terminate = function() { 68 | this.worker.postMessage({'c': 't'}); 69 | } 70 | 71 | XpraProtocolWorkerHost.prototype.send = function(packet) { 72 | this.worker.postMessage({'c': 's', 'p': packet}); 73 | } 74 | 75 | XpraProtocolWorkerHost.prototype.set_packet_handler = function(callback, ctx) { 76 | this.packet_handler = callback; 77 | this.packet_ctx = ctx; 78 | } 79 | 80 | XpraProtocolWorkerHost.prototype.set_cipher_in = function(caps, key) { 81 | this.worker.postMessage({'c': 'z', 'p': caps, 'k': key}); 82 | } 83 | 84 | XpraProtocolWorkerHost.prototype.set_cipher_out = function(caps, key) { 85 | this.worker.postMessage({'c': 'x', 'p': caps, 'k': key}); 86 | } 87 | 88 | 89 | 90 | /* 91 | The main Xpra wire protocol 92 | */ 93 | function XpraProtocol() { 94 | 95 | this.is_worker = false; 96 | this.packet_handler = null; 97 | this.packet_ctx = null; 98 | this.websocket = null; 99 | this.raw_packets = []; 100 | this.cipher_in = null; 101 | this.cipher_in_block_size = null; 102 | this.cipher_out = null; 103 | this.rQ = []; // Receive queue 104 | this.sQ = []; // Send queue 105 | this.mQ = []; // Worker message queue 106 | this.header = []; 107 | 108 | //Queue processing via intervals 109 | this.process_interval = 0; //milliseconds 110 | } 111 | 112 | XpraProtocol.prototype.open = function(uri) { 113 | var me = this; 114 | // (re-)init 115 | this.raw_packets = []; 116 | this.rQ = []; 117 | this.sQ = []; 118 | this.mQ = []; 119 | this.header = []; 120 | this.websocket = null; 121 | // connect the socket 122 | try { 123 | this.websocket = new WebSocket(uri, 'binary'); 124 | } 125 | catch (e) { 126 | this.packet_handler(['error', ""+e], this.packet_ctx); 127 | return; 128 | } 129 | this.websocket.binaryType = 'arraybuffer'; 130 | this.websocket.onopen = function () { 131 | me.packet_handler(['open'], me.packet_ctx); 132 | }; 133 | this.websocket.onclose = function () { 134 | me.packet_handler(['close'], me.packet_ctx); 135 | }; 136 | this.websocket.onerror = function () { 137 | me.packet_handler(['error'], me.packet_ctx); 138 | }; 139 | this.websocket.onmessage = function (e) { 140 | // push arraybuffer values onto the end 141 | me.rQ.push(new Uint8Array(e.data)); 142 | setTimeout(function() { 143 | me.process_receive_queue(); 144 | }, this.process_interval); 145 | }; 146 | } 147 | 148 | XpraProtocol.prototype.close = function() { 149 | if (this.websocket) { 150 | this.websocket.onopen = null; 151 | this.websocket.onclose = null; 152 | this.websocket.onerror = null; 153 | this.websocket.onmessage = null; 154 | this.websocket.close(); 155 | this.websocket = null; 156 | } 157 | } 158 | 159 | XpraProtocol.prototype.protocol_error = function(msg) { 160 | console.error("protocol error:", msg); 161 | //make sure we stop processing packets and events: 162 | this.websocket.onopen = null; 163 | this.websocket.onclose = null; 164 | this.websocket.onerror = null; 165 | this.websocket.onmessage = null; 166 | this.header = []; 167 | this.rQ = []; 168 | //and just tell the client to close (it may still try to re-connect): 169 | this.packet_handler(['close', msg]); 170 | } 171 | 172 | XpraProtocol.prototype.process_receive_queue = function() { 173 | var i = 0, j = 0; 174 | if (this.header.length<8 && this.rQ.length>0) { 175 | //add from receive queue data to header until we get the 8 bytes we need: 176 | while (this.header.length<8 && this.rQ.length>0) { 177 | var slice = this.rQ[0]; 178 | var needed = 8-this.header.length; 179 | var n = Math.min(needed, slice.length); 180 | //console.log("header size", this.header.length, ", adding", n, "bytes from", slice.length); 181 | //copy at most n characters: 182 | for (i = 0; i < n; i++) { 183 | this.header.push(slice[i]); 184 | } 185 | if (slice.length>needed) { 186 | //replace the slice with what is left over: 187 | this.rQ[0] = slice.subarray(n); 188 | } 189 | else { 190 | //this slice has been fully consumed already: 191 | this.rQ.shift(); 192 | } 193 | } 194 | 195 | //verify the header format: 196 | if (this.header[0] !== ord("P")) { 197 | var msg = "invalid packet header format: " + this.header[0]; 198 | if (this.header.length>1) { 199 | msg += ": "; 200 | for (var c in this.header) { 201 | msg += String.fromCharCode(c); 202 | } 203 | } 204 | this.protocol_error(msg); 205 | return; 206 | } 207 | } 208 | 209 | if (this.header.length<8) { 210 | //we need more data to continue 211 | return; 212 | } 213 | 214 | var proto_flags = this.header[1]; 215 | var proto_crypto = proto_flags & 0x2; 216 | if (proto_flags!=0) { 217 | // check for crypto protocol flag 218 | if (!(proto_crypto)) { 219 | this.protocol_error("we can't handle this protocol flag yet: "+proto_flags); 220 | return; 221 | } 222 | } 223 | 224 | var level = this.header[2]; 225 | if (level & 0x20) { 226 | this.protocol_error("lzo compression is not supported"); 227 | return; 228 | } 229 | var index = this.header[3]; 230 | if (index>=20) { 231 | this.protocol_error("invalid packet index: "+index); 232 | return; 233 | } 234 | var packet_size = 0; 235 | for (i=0; i<4; i++) { 236 | packet_size = packet_size*0x100; 237 | packet_size += this.header[4+i]; 238 | } 239 | 240 | // work out padding if necessary 241 | var padding = 0 242 | if (proto_crypto) { 243 | padding = (this.cipher_in_block_size - packet_size % this.cipher_in_block_size); 244 | packet_size += padding; 245 | } 246 | 247 | // verify that we have enough data for the full payload: 248 | var rsize = 0; 249 | for (i=0,j=this.rQ.length;ineeded) { 273 | //add part of this slice: 274 | packet_data.set(slice.subarray(0, needed), rsize); 275 | rsize += needed; 276 | this.rQ[0] = slice.subarray(needed); 277 | } 278 | else { 279 | //add this slice in full: 280 | packet_data.set(slice, rsize); 281 | rsize += slice.length; 282 | this.rQ.shift(); 283 | } 284 | } 285 | } 286 | 287 | // decrypt if needed 288 | if (proto_crypto) { 289 | this.cipher_in.update(forge.util.createBuffer(uintToString(packet_data))); 290 | var decrypted = this.cipher_in.output.getBytes(); 291 | packet_data = []; 292 | for (i=0; i= 0; i--) { 390 | bdata += String.fromCharCode(padding_size); 391 | }; 392 | this.cipher_out.update(forge.util.createBuffer(bdata)); 393 | bdata = this.cipher_out.output.getBytes(); 394 | } 395 | var actual_size = bdata.length; 396 | //convert string to a byte array: 397 | var cdata = []; 398 | for (var i=0; i=0; i--) 410 | header.push((payload_size >> (8*i)) & 0xFF); 411 | //concat data to header, saves an intermediate array which may or may not have 412 | //been optimised out by the JS compiler anyway, but it's worth a shot 413 | header = header.concat(cdata); 414 | //debug("send("+packet+") "+cdata.length+" bytes in packet for: "+bdata.substring(0, 32)+".."); 415 | // put into buffer before send 416 | if (this.websocket) { 417 | this.websocket.send((new Uint8Array(header)).buffer); 418 | } 419 | } 420 | } 421 | 422 | XpraProtocol.prototype.process_message_queue = function() { 423 | while(this.mQ.length !== 0){ 424 | var packet = this.mQ.shift(); 425 | 426 | if(!packet){ 427 | return; 428 | } 429 | 430 | var raw_draw_buffer = (packet[0] === 'draw') && (packet[6] !== 'scroll'); 431 | postMessage({'c': 'p', 'p': packet}, raw_draw_buffer ? [packet[7].buffer] : []); 432 | } 433 | } 434 | 435 | XpraProtocol.prototype.send = function(packet) { 436 | this.sQ[this.sQ.length] = packet; 437 | var me = this; 438 | setTimeout(function() { 439 | me.process_send_queue(); 440 | }, this.process_interval); 441 | } 442 | 443 | XpraProtocol.prototype.set_packet_handler = function(callback, ctx) { 444 | this.packet_handler = callback; 445 | this.packet_ctx = ctx; 446 | } 447 | 448 | XpraProtocol.prototype.set_cipher_in = function(caps, key) { 449 | this.cipher_in_block_size = 32; 450 | // stretch the password 451 | var secret = forge.pkcs5.pbkdf2(key, caps['cipher.key_salt'], caps['cipher.key_stretch_iterations'], this.cipher_in_block_size); 452 | // start the cipher 453 | this.cipher_in = forge.cipher.createDecipher('AES-CBC', secret); 454 | this.cipher_in.start({iv: caps['cipher.iv']}); 455 | } 456 | 457 | XpraProtocol.prototype.set_cipher_out = function(caps, key) { 458 | this.cipher_out_block_size = 32; 459 | // stretch the password 460 | var secret = forge.pkcs5.pbkdf2(key, caps['cipher.key_salt'], caps['cipher.key_stretch_iterations'], this.cipher_out_block_size); 461 | // start the cipher 462 | this.cipher_out = forge.cipher.createCipher('AES-CBC', secret); 463 | this.cipher_out.start({iv: caps['cipher.iv']}); 464 | } 465 | 466 | 467 | /* 468 | If we are in a web worker, set up an instance of the protocol 469 | */ 470 | if (!(typeof window == "object" && typeof document == "object" && window.document === document)) { 471 | // some required imports 472 | // worker imports are relative to worker script path 473 | // make protocol instance 474 | var protocol = new XpraProtocol(); 475 | protocol.is_worker = true; 476 | // we create a custom packet handler which posts packet as a message 477 | protocol.set_packet_handler(function (packet, ctx) { 478 | var raw_draw_buffer = (packet[0] === 'draw') && (packet[6] !== 'scroll'); 479 | postMessage({'c': 'p', 'p': packet}, raw_draw_buffer ? [packet[7].buffer] : []); 480 | }, null); 481 | // attach listeners from main thread 482 | self.addEventListener('message', function(e) { 483 | var data = e.data; 484 | switch (data.c) { 485 | case 'o': 486 | protocol.open(data.u); 487 | break; 488 | case 's': 489 | protocol.send(data.p); 490 | break; 491 | case 'x': 492 | protocol.set_cipher_out(data.p, data.k); 493 | break; 494 | case 'z': 495 | protocol.set_cipher_in(data.p, data.k); 496 | break; 497 | case 'c': 498 | // close the connection 499 | protocol.close(); 500 | break; 501 | case 't': 502 | // terminate the worker 503 | self.close(); 504 | break; 505 | default: 506 | postMessage({'c': 'l', 't': 'got unknown command from host'}); 507 | }; 508 | }, false); 509 | // tell host we are ready 510 | postMessage({'c': 'r'}); 511 | } 512 | 513 | 514 | export default XpraProtocol; 515 | -------------------------------------------------------------------------------- /src/layer.js: -------------------------------------------------------------------------------- 1 | import Utilities from '../lib/Utilities.js'; 2 | import Decoder from '../lib/Decoder.js'; 3 | import {MediaSourceUtil, MediaSourceConstants} from '../lib/MediaSourceUtil.js'; 4 | import Zlib from 'zlibjs'; 5 | import LZ4 from 'lz4'; 6 | 7 | // TODO: Separate into VideoDecoder class 8 | 9 | const DEFAULT_BOX_COLORS = { 10 | 'png': 'yellow', 11 | 'h264': 'blue', 12 | 'vp8': 'green', 13 | 'rgb24': 'orange', 14 | 'rgb32': 'red', 15 | 'jpeg': 'purple', 16 | 'png/P': 'indigo', 17 | 'png/L': 'teal', 18 | 'h265': 'khaki', 19 | 'vp9': 'lavender', 20 | 'mpeg4': 'black', 21 | 'scroll': 'brown' 22 | }; 23 | 24 | export default class Layer { 25 | constructor(wid, props) { 26 | this.props = props || {}; 27 | this.wid = wid; 28 | this.canvas = null; 29 | this.canvas_ctx = null; 30 | this.video = null; 31 | this.offscreen_canvas = null; 32 | this.offscreen_canvas_ctx = null; 33 | this.offscreen_canvas_mode = '2d'; 34 | this.paint_queue = []; 35 | this.paint_pending = 0; 36 | this.broadway_decoder = null; 37 | this.video_buffers = []; 38 | this.video_buffers_count = 0; 39 | this.broadway_paint_location = []; 40 | this.media_source = null; 41 | this.video_source_ready = false; 42 | this.video_source_buffer = null; 43 | } 44 | 45 | destroy() { 46 | this.closeVideo(); 47 | this.closeBroadway(); 48 | if ( this.canvas ) { 49 | if ( this.canvas.parentNode ) { 50 | this.canvas.parentNode.removeChild(this.canvas); 51 | } 52 | this.canvas = null; 53 | } 54 | } 55 | 56 | init(canvas, geom) { 57 | this.canvas = canvas; 58 | this.canvas_ctx = this.canvas.getContext('2d'); 59 | 60 | this.offscreen_canvas = document.createElement('canvas'); 61 | this.offscreen_canvas_ctx = this.offscreen_canvas.getContext('2d'); 62 | 63 | this.updateCanvases(geom); 64 | } 65 | 66 | updateCanvases(geom) { 67 | if ( this.canvas ) { 68 | this.canvas.width = geom.w; 69 | this.canvas.height = geom.h; 70 | } 71 | if ( this.offscreen_canvas ) { 72 | this.offscreen_canvas.width = geom.w; 73 | this.offscreen_canvas.height = geom.h; 74 | } 75 | } 76 | 77 | paint() { 78 | const item = Array.prototype.slice.call(arguments); 79 | this.paint_queue.push(item); 80 | this.paintQueue(); 81 | } 82 | 83 | paintQueue() { 84 | let now = Utilities.monotonicTime(); 85 | while ((this.paint_pending === 0 || (now - this.paint_pending) >= 2000) && this.paint_queue.length > 0) { 86 | this.paint_pending = now; 87 | const item = this.paint_queue.shift(); 88 | this.paintItem(...item); 89 | now = Utilities.monotonicTime(); 90 | } 91 | } 92 | 93 | paintItem(x, y, width, height, coding, img_data, packet_sequence, rowstride, options, decode_callback) { 94 | var enc_width = width; 95 | var enc_height = height; 96 | var scaled_size = options.scaled_size; 97 | if ( scaled_size ) { 98 | enc_width = scaled_size[0]; 99 | enc_height = scaled_size[1]; 100 | } 101 | 102 | const paint_box = (color, px, py, pw, ph) => { 103 | this.offscreen_canvas_ctx.strokeStyle = color; 104 | this.offscreen_canvas_ctx.lineWidth = '2'; 105 | this.offscreen_canvas_ctx.strokeRect(px, py, pw, ph); 106 | }; 107 | 108 | const painted = (skip_box) => { 109 | this.paint_pending = 0; 110 | decode_callback(this.client); 111 | if (this.debug && !skip_box) { 112 | var color = DEFAULT_BOX_COLORS[coding] || 'white'; 113 | paint_box(color, x, y, width, height); 114 | } 115 | this.paintQueue(); 116 | }; 117 | 118 | const paint_error = (e) => { 119 | console.error('error painting', coding, e); 120 | this.paint_pending = 0; 121 | decode_callback(this.client, '' + e); 122 | this.paintQueue(); 123 | }; 124 | 125 | const decodeRbg32 = () => { 126 | this.nonVideoPaint(coding); 127 | 128 | var img = this.offscreen_canvas_ctx.createImageData(width, height); 129 | var inflated; 130 | 131 | //show('options='+(options).toSource()); 132 | if (options !== null && options.zlib > 0) { 133 | //show('decompressing '+img_data.length+' bytes of '+coding+'/zlib'); 134 | inflated = new Zlib.Inflate(img_data).decompress(); 135 | //show('rgb32 data inflated from '+img_data.length+' to '+inflated.length+' bytes'); 136 | img_data = inflated; 137 | } else if (options !== null && options.lz4 > 0) { 138 | // in future we need to make sure that we use typed arrays everywhere... 139 | var d; 140 | if (img_data.subarray) { 141 | d = img_data.subarray(0, 4); 142 | } else { 143 | d = img_data.slice(0, 4); 144 | } 145 | 146 | // will always be little endian 147 | var length = d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24); 148 | // decode the LZ4 block 149 | inflated = new Buffer(length); 150 | 151 | var uncompressedSize; 152 | if (img_data.subarray) { 153 | uncompressedSize = LZ4.decodeBlock(img_data.subarray(4), inflated); 154 | } else { 155 | uncompressedSize = LZ4.decodeBlock(img_data.slice(4), inflated); 156 | } 157 | img_data = inflated.slice(0, uncompressedSize); 158 | } 159 | 160 | // set the imagedata rgb32 method 161 | if (img_data.length > img.data.length) { 162 | paint_error('data size mismatch: wanted ' + img.data.length + ', got ' + img_data.length + ', stride=' + rowstride); 163 | } else { 164 | img.data.set(img_data); 165 | this.offscreen_canvas_ctx.putImageData(img, x, y); 166 | painted(); 167 | } 168 | }; 169 | 170 | const decodeJpeg = () => { 171 | this.nonVideoPaint(coding); 172 | 173 | var j = new Image(); 174 | j.onload = () => { 175 | if (j.width === 0 || j.height === 0) { 176 | paint_error('invalid image size: ' + j.width + 'x' + j.height); 177 | } else { 178 | this.offscreen_canvas_ctx.drawImage(j, x, y); 179 | painted(); 180 | } 181 | }; 182 | j.onerror = function() { 183 | paint_error('failed to load into image tag'); 184 | }; 185 | j.src = 'data:image/' + coding + ';base64,' + this.arrayBufferToBase64(img_data); 186 | }; 187 | 188 | const decodeBroadway = () => { 189 | var frame = options.frame || -1; 190 | if (frame === 0) { 191 | this.closeBroadway(); 192 | } 193 | if (!this.broadway_decoder) { 194 | this.initBroadway(enc_width, enc_height, width, height); 195 | } 196 | 197 | this.broadway_paint_location = [x, y]; 198 | // we can pass a buffer full of NALs to decode() directly 199 | // as long as they are framed properly with the NAL header 200 | if (!Array.isArray(img_data)) { 201 | img_data = Array.from(img_data); 202 | } 203 | this.broadway_decoder.decode(img_data); 204 | // broadway decoding is synchronous: 205 | // (and already painted via the onPictureDecoded callback) 206 | painted(); 207 | }; 208 | 209 | const decodeMp4 = () => { 210 | var frame = options.frame || -1; 211 | if (frame === 0) { 212 | this.closeVideo(); 213 | } 214 | 215 | if (!this.video) { 216 | var profile = options.profile || 'baseline'; 217 | var level = options.level || '3.0'; 218 | this.initVideo(width, height, coding, profile, level); 219 | } else { 220 | //keep it above the div: 221 | this.video.style.zIndex = this.div.css('z-index') + 1; 222 | } 223 | 224 | if (img_data.length > 0) { 225 | this.video_buffers.push(img_data); 226 | if (this.video.paused) { 227 | this.video.play(); 228 | } 229 | this.pushVideoBuffers(); 230 | //try to throttle input: 231 | var delay = Math.max(10, 50 * (this.video_buffers.length - 25)); 232 | setTimeout(function() { 233 | painted(); 234 | }, delay); 235 | //this._debug('video queue: ', this.video_buffers.length); 236 | } 237 | }; 238 | 239 | const decodeScroll = () => { 240 | this.nonVideoPaint(coding); 241 | 242 | for (var i = 0, j = img_data.length;i < j;++i) { 243 | var scroll_data = img_data[i]; 244 | var sx = scroll_data[0], 245 | sy = scroll_data[1], 246 | sw = scroll_data[2], 247 | sh = scroll_data[3], 248 | xdelta = scroll_data[4], 249 | ydelta = scroll_data[5]; 250 | 251 | this.offscreen_canvas_ctx.drawImage(this.offscreen_canvas, sx, sy, sw, sh, sx + xdelta, sy + ydelta, sw, sh); 252 | if (this.debug) { 253 | paint_box('brown', sx + xdelta, sy + ydelta, sw, sh); 254 | } 255 | } 256 | painted(true); 257 | }; 258 | 259 | try { 260 | if (coding === 'rgb32') { 261 | decodeRbg32(); 262 | } else if (coding === 'jpeg' || coding === 'png') { 263 | decodeJpeg(); 264 | } else if (coding === 'h264') { 265 | decodeBroadway(); 266 | } else if (coding === 'h264+mp4' || coding === 'vp8+webm' || coding === 'mpeg4+mp4') { 267 | decodeMp4(); 268 | } else if (coding === 'scroll') { 269 | decodeScroll(); 270 | } else { 271 | paint_error('unsupported encoding'); 272 | } 273 | } catch (e) { 274 | paint_error(e); 275 | } 276 | } 277 | 278 | draw() { 279 | if ( this.canvas && this.offscreen_canvas ) { 280 | this.canvas_ctx.drawImage(this.offscreen_canvas, 0, 0); 281 | } 282 | } 283 | 284 | nonVideoPaint(coding) { 285 | if ( this.video && this.video.style.zIndex !== '-1' ) { 286 | this.video.style.zIndex = '-1'; 287 | var width = this.video.getAttribute('width'); 288 | var height = this.video.getAttribute('height'); 289 | this.offscreen_canvas_ctx.drawImage(this.video, 0, 0, width, height); 290 | } 291 | } 292 | 293 | arrayBufferToBase64(uintArray) { 294 | // apply in chunks of 10400 to avoid call stack overflow 295 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 296 | var s = ''; 297 | var skip = 10400; 298 | var i, len; 299 | if ( uintArray.subarray ) { 300 | for ( i = 0, len = uintArray.length; i < len; i += skip ) { 301 | s += String.fromCharCode.apply(null, uintArray.subarray(i, Math.min(i + skip, len))); 302 | } 303 | } else { 304 | for (i = 0, len = uintArray.length; i < len; i += skip ) { 305 | s += String.fromCharCode.apply(null, uintArray.slice(i, Math.min(i + skip, len))); 306 | } 307 | } 308 | return window.btoa(s); 309 | } 310 | 311 | initBroadway(enc_width, enc_height, width, height) { 312 | this.broadway_decoder = new Decoder({ 313 | 'rgb': true, 314 | 'size': { 315 | width: enc_width, 316 | height: enc_height 317 | } 318 | }); 319 | 320 | this.broadway_paint_location = [0, 0]; 321 | this.broadway_decoder.onPictureDecoded = (buffer, p_width, p_height, infos) => { 322 | if ( !this.broadway_decoder ) { 323 | return; 324 | } 325 | 326 | var img = this.offscreen_canvas_ctx.createImageData(p_width, p_height); 327 | img.data.set(buffer); 328 | 329 | var x = this.broadway_paint_location[0]; 330 | var y = this.broadway_paint_location[1]; 331 | this.offscreen_canvas_ctx.putImageData(img, x, y); 332 | 333 | if ( enc_width !== width || enc_height !== height ) { 334 | this.offscreen_canvas_ctx.drawImage(this.offscreen_canvas, x, y, p_width, p_height, x, y, width, height); 335 | } 336 | }; 337 | } 338 | 339 | closeBroadway() { 340 | this.broadway_decoder = null; 341 | } 342 | 343 | initVideo(width, height, coding, profile, level) { 344 | this.media_source = MediaSourceUtil.getMediaSource(); 345 | 346 | if ( this.debug ) { 347 | MediaSourceUtil.addMediaSourceEventDebugListeners(this.media_source, 'video'); 348 | } 349 | 350 | //