Index: noVNC/include/esd.js =================================================================== --- noVNC/include/esd.js (revision 0) +++ noVNC/include/esd.js (working copy) @@ -0,0 +1,349 @@ +/* + * EsounD Server + * Copyright 2013 Samuel Mannehed for Cendio AB + */ + +function ESD(defaults) { +"use strict"; + +var that = {}, // Public API methods + conf = {}, // Configuration attributes + + // Pre-declare private functions used before definitions (jslint) + handle_message, connect_audio, disconnect_audio, buffer_audio, + parse_stream, parse_format, parse_rate, + + // + // Private ESD namespace variables + // + esd_host = '', + esd_port = 4910, + esd_path = '', + + ws = null, // Websock object + + ws_loop_timeout = 1000, // time until we loop the ws connection + ws_loop_timer = null, // timer for looping the ws connection + + authenticated = false, // true when the esd connection is authenticated + little_endian = false, + sixteen_bits = false, + stream = false, // true while streaming data + play = false, // true while playing sound + nrOfChannels = 1, + rate = 0, // Sample rate of audio + name = "", // name of esd stream + + context = null, // audio context + node = null, // audio processing node + buff = new Array(); // buffer for audio data + +// Configuration attributes +Util.conf_defaults(conf, that, defaults, [ + ['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'] + ]); + +// +// Setup routines +// + +// Create the public API interface and initialize values that stay +// constant across connect/disconnect +function constructor() { + + ws = new Websock(); + ws.on('message', handle_message); + ws.on('open', function() { + console.log("ESD: WebSocket on-open event"); + }); + ws.on('close', function(e) { + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) + msg += ", reason: " + e.reason; + msg += ")"; + } + console.log("ESD: WebSocket on-close event, msg: " + msg); + + // Loop + ws_loop_timer = setTimeout(function() { + connect(); + }, ws_loop_timeout); + }); + ws.on('error', function(e) { + console.log("ESD: WebSocket on-error event, data: " + e.data); + if (ws_loop_timer) { + console.log("ESD: Clearing websocket loop timer"); + clearTimeout(ws_loop_timer); + ws_loop_timer = null; + } + }); + + ws.init(); + + /* Check web-socket-js if no builtin WebSocket support */ + if (Websock_native) { + console.log("ESD: Using native WebSockets"); + } else { + console.log("ESD: Using web-socket-js bridge. Flash version: " + + Util.Flash.version); + } + + try { + if (window.AudioContext) + context = new AudioContext(); + else + context = new webkitAudioContext(); + } catch(e) { + console.log("ESD: Web Audio API is not supported in this browser: " + e); + } + + return that; // Return the public API interface +} + +// On iOS devices the sound needs to start from an user initiated event +window.addEventListener('touchstart', function() { + + if (context.createScriptProcessor) + node = context.createScriptProcessor(4096, 0, 1); + else + node = context.createJavaScriptNode(4096, 0, 1); + + var source = context.createBufferSource(); + var buffer = context.createBuffer(1, 1024, context.sampleRate) + var data = buffer.getChannelData(0); + for (var i = 0; i < data.length; i++) + data[i] = 0; + source.buffer = buffer; + source.loop = true; + source.connect(node); + node.connect(context.destination); + + source.start(0); + +}, false); + +function connect() { + var uri; + + if (typeof UsingSocketIO !== "undefined") { + uri = "http://" + esd_host + ":" + esd_port + "/" + esd_path; + } else { + if (conf.encrypt) + uri = "wss://"; + else + uri = "ws://"; + uri += esd_host + ":" + esd_port + "/" + esd_path; + } + console.log("ESD: connecting to " + uri); + ws.open(uri, ['binary','base64','audio']); +} + +function disconnect() { + ws.close(); +} + +// +// Utility routines +// + +handle_message = function() { + if (ws.rQlen() === 0) { return; } + + if (stream && play) { + buffer_audio(); + return; + } + + var opcode, esdkey, endian; + + // If we are not authenticated yet + if (!authenticated) { + if (ws.rQwait("init", 20, 0)) { return; } + + esdkey = ws.rQshiftBytes(16); + endian = ws.rQshiftStr(4); + little_endian = (endian == 'NDNE'); + if (little_endian) + ws.send([1,0,0,0]); + else + ws.send([0,0,0,1]); + authenticated = true; + + console.log("ESD: authenticated"); + + } else { + opcode = ws.rQshiftBytes(4); + + switch (opcode[0]) { + case 1: //lock + case 2: //unlock + case 3: //stream-play + if (ws.rQwait("stream-play", 136, 4)) { return; } + + // Parse the header + parse_format(ws.rQshiftBytes(4)); + parse_rate(ws.rQshiftBytes(4)); + name = ws.rQshiftStr(128); + + console.log("ESD: --- Audio header --- "); + console.log((sixteen_bits ? "16bit " : "") + + ((nrOfChannels == 1) ? "mono " : "stereo ") + + (stream ? "stream " : "") + + (play ? "play" : "")); + console.log(rate + " Hz"); + console.log(name); + + connect_audio(); + case 5: //stream-mon + case 6: //sample-cache + case 7: //sample-free + case 8: //sample-play + case 9: //sample-loop + case 10: //sample-stop + case 11: //sample-kill + case 12: //standby + case 13: //resume + case 14: //sample-getid + case 15: //stream-filter + case 16: //server-info + case 17: //server-all-info + case 18: //subscribe + case 19: //unsubjcribe + case 20: //stream-pan + case 21: //sample-pan + case 22: //standby-mode + case 23: //latency + } + } +}; + +connect_audio = function() { + // Reset the node + if (node != null) + node.disconnect(); + + if (context.createScriptProcessor) + node = context.createScriptProcessor(4096, 0, nrOfChannels); + else + node = context.createJavaScriptNode(4096, 0, nrOfChannels); + node.onaudioprocess = parse_stream; + node.connect(context.destination); +}; + +disconnect_audio = function() { + + node.onaudioprocess = null; + node.disconnect(); + + authenticated = false; + little_endian = false; + sixteen_bits = false; + stream = false; + play = false; + nrOfChannels = 1; + rate = 0; + name = ""; + + buff = new Array(); + + console.log("ESD: Closed audio connection"); +}; + +buffer_audio = function() { + var ws_data = ws.rQshiftBytes(ws.rQlen()); + var l = buff.length; + buff = buff.concat(ws_data); + console.log("buffered " + (buff.length - l)); +}; + +parse_stream = function(e) { + var audio, n, channels, chan_positions, i16; + + if (buff.length > 0) { + // Take buffered data + audio = buff.splice(0, e.outputBuffer.length*4); + console.log("playing " + audio.length) + + n = e.outputBuffer.numberOfChannels; + channels = new Array(n); + chan_positions = new Array(n); + for (var c = 0; c < n; c++) { + channels[c] = e.outputBuffer.getChannelData(c); + channels[c].sampleRate = rate; + chan_positions[c] = 0; + } + + for (var i = 0; i < audio.length; i += 2) { + + // handle endian and convert to 16bit + if (little_endian) + i16 = audio[i] + (audio[i+1] << 8); + else + i16 = (audio[i+1] << 8) + audio[i]; + + // handle signed + if (i16 > 32767) + i16 -= 65536; + + // put audio on the channels + for (var c = 0; c < n; c++) { + if ((i/2) % n == c) { + (channels[c])[chan_positions[c]] = i16 / 32767; + chan_positions[c]++; + } + } + } + } else { + disconnect_audio(); + } +}; + +parse_format = function(ws_data) { + var format = "", length = ws_data.length; + if (little_endian) { + for (var i = length - 1; i >= 0; i--) + format += ws_data[i].toString(16); + } else { + for (var i = 0; i < length; i++) + format += ws_data[i].toString(16); + } + sixteen_bits = format.charAt(format.length - 1) == 1; + nrOfChannels = format.charAt(format.length - 2); + stream = format.charAt(format.length - 3) == 0; + play = format.charAt(format.length - 4) == 1; +}; + +parse_rate = function(ws_data) { + var length = ws_data.length; + if (little_endian) { + for (var i = length - 1; i >= 0; i--) + rate += ws_data[i] * Math.pow(2, 8 * i); + } else { + for (var i = 0; i < length; i++) + rate += ws_data[i] * Math.pow(2, 8 * i); + } +}; + +// +// Public API interface functions +// + +that.connect = function(host, port, path) { + esd_host = host; + esd_port = port; + esd_path = (path !== undefined) ? path : ""; + + if ((!esd_host) || (!esd_port)) + return fail("Must set host and port"); + connect(); +}; + +that.disconnect = function() { + disconnect(); +}; + +return constructor(); // Return the public API interface + +} // End of ESD() Index: noVNC/include/ui.js =================================================================== --- noVNC/include/ui.js (revision 28068) +++ noVNC/include/ui.js (working copy) @@ -27,7 +27,7 @@ Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "input.js", "display.js", "jsunzip.js", "rfb.js", - "keysym.js"]); + "keysym.js", "esd.js"]); var UI = { @@ -113,6 +113,7 @@ 'onClipboard': UI.clipReceive, 'onFBUComplete': UI.FBUComplete, 'onDesktopName': UI.updateDocumentTitle}); + UI.esd = ESD({'encrypt': (window.location.protocol === "https:")}); autoconnect = WebUtil.getQueryVar('autoconnect', false); if (autoconnect === 'true' || autoconnect == '1') { @@ -663,6 +664,7 @@ UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); UI.rfb.connect(host, port, password, path); + UI.esd.connect(host, port, path); //Close dialog. setTimeout(UI.setBarPosition, 100); @@ -672,6 +674,7 @@ disconnect: function() { UI.closeSettingsMenu(); UI.rfb.disconnect(); + UI.esd.disconnect(); $D('noVNC_logo').style.display = "block"; UI.connSettingsOpen = false;