// Host and Client (hC) Module // Version 1.3 (7:50 PM Fri October 27, 2017) // Written by: James D. Miller // This module is dependent on gwModule.js (referenced here as gW). var hC = (function() { // To insist on tighter code: e.g. globals, etc... "use strict"; // A few globals within hC. ///////////////////////////////////////////////// var socket = null; var nodeServerURL, serverArray; var chatStyleToggle = true; var timer = {}; timer.start = null; timer.end = null; timer.pingArray = []; var clientCanvas, ctx; var videoMirror, videoStream; var chkRequestStream, chkLocalCursor; // Key values. var keyMap = {'49':'1','50':'2','51':'3','52':'4','53':'5','54':'6','55':'7','56':'8', '70':'f', '65':'a','83':'s','68':'d','87':'w', '74':'j','75':'k','76':'l','73':'i', '16':'sh','32':'sp'}; //sh:shift, sp:space // Mouse and keyboard (mK) from non-host clients. var mK = {}; mK.name = null; // Key values, cso (client side only) for use only by the client, not to be sent over network // to the host. var keyMap_cso = {'16':'key_shift', '17':'key_ctrl', '80':'key_p'} var mK_cso = {}; // The client name of this user. This global is only used on the client page and // is some increment of u1, u2, etc for network clients. var newClientName = null; // A global that points at a "Client" or clientlike object. For the host, this will point at the // "Client" object of the client that most recently attempts to connect. On the client page, // this will simply keep this structure and replace the 'notyetnamed' with the name of that // client. var cl = {'name':'notyetnamed'}; var rtc_choke = false; var fileName = "hostAndClient.js"; ////////////////////////////////////////////////// // Object prototypes ////////////////////////////////////////////////// function RTC( pars) { this.user1 = pars.user1 || null; this.user2 = pars.user2 || null; this.streamRequested = pars.streamRequested || null; this.pc = null; this.dataChannel = null; } RTC.prototype.shutdown = function() { //console.log('pc:'+JSON.stringify(this.pc)); //console.log('dataChannel:'+JSON.stringify(this.dataChannel)); // Close then nullify any references to the datachannel and the p2p connection. if (this.dataChannel) { //console.log('a'); this.dataChannel.close(); } if (this.pc) { var senders = this.pc.getSenders(); if (senders.length >= 1) { //console.log('senders length = ' + senders.length); this.pc.removeTrack( senders[0]); senders = this.pc.getSenders(); //console.log('senders length = ' + senders.length); } //console.log('b'); this.pc.close(); } if (this.dataChannel) { //console.log('c'); this.dataChannel = null; } if (this.pc) { //console.log('d'); this.pc = null; } } ////////////////////////////////////////////////// // Functions supporting the socket.io connections ////////////////////////////////////////////////// function connect_and_listen( hostOrClient) { if (hostOrClient != 'host') { // Disable the client connect button for 4 seconds after use. $('#ConnectButton').html('Wait 4'); $('#ConnectButton').prop('disabled', true); window.setTimeout(function(){$('#ConnectButton').html('Wait 3');}, 1000); window.setTimeout(function(){$('#ConnectButton').html('Wait 2');}, 2000); window.setTimeout(function(){$('#ConnectButton').html('Wait 1');}, 3000); window.setTimeout(function(){ $('#ConnectButton').prop('disabled', false); $('#ConnectButton').html('Connect'); }, 4000); } var nodeString = $('#nodeServer').val(); if (nodeString == "") { // Use one in the list as a default. nodeString = serverArray[0]; $('#nodeServer').val( nodeString); } if (nodeString.includes("heroku")) { var urlPrefix = "https://" } else { var urlPrefix = "http://" } nodeServerURL = urlPrefix + nodeString; //console.log("URL=" + nodeServerURL); // Use jquery to load the socket.io client code. $.getScript( nodeServerURL + "/socket.io/socket.io.js", function() { // This callback function will run after the getScript finishes loading the socket.io client. console.log("socket.io script has loaded."); // If there are already active network connections, close them before making new ones. This is // the case if the client repeatedly clicks the connect button trying to get a preferred color. if (socket) { if (hostOrClient != 'host') { // Send a message to the host (via socket.io server) to shutdown RTC connections. if (newClientName) { if (videoMirror.srcObject) videoMirror.srcObject = null; //console.log("in client's getScript callback, name = " + newClientName); // Trigger client shutdown at the host. socket.emit('shutDown-p2p-deleteClient', newClientName); } } window.setTimeout( function() { // Close socket.io connection after waiting a bit for the p2p connections to close. socket.disconnect(); }, 500); } // Delay this even longer than the socket.disconnect() above (to be sure the disconnect is done). window.setTimeout( function() { var roomName = $('#roomName').val(); if (roomName != "") { // the HTML limit is set to 9 (so you can try a little more then 7, but then get some advice to limit it to 7) if (roomName.length <= 7) { // Here is where the socket.io client initiates it's connection to the server. The 'query' parameter is not // needed here but is just shown to document the form of the query string with a two parameter example. socket = io.connect( nodeServerURL, {'forceNew':true, 'query':'par1=P1&par2=P2'}); init_socket_listeners( roomName, hostOrClient); } else { displayMessage('The name should have 7 characters or less.'); } } else { displayMessage('Type in a short "Room" name, then click the "Connect" button.'); } }, 600); // Use the "fail" method of getScript to report a connection problem. }).fail(function( jqxhr, settings, exception) { displayMessage('The node server is not responding. Try changing to a different server.'); }); } function displayMessage( msgText) { // Every other line, toggle the background shading. if (chatStyleToggle) { var styleString = 'style="background: #efefef;"'; } else { var styleString = 'style="background: #d9d9d9;"'; } $('#messages').prepend('<li '+styleString+'>'+ msgText +'</li>'); chatStyleToggle = !chatStyleToggle; } function init_chatFeatures( hostOrClient) { serverArray = ['secure-retreat-15768.herokuapp.com', 'localhost:3000', '192.168.1.106:3000', '192.168.1.109:3000', //David's computer '192.168.1.116:3000', //RPi '192.168.1.117:3000']; //Laptop // Use jquery to loop over the serverArray and build the URL datalist. jQuery.each( serverArray, function( i, val ) { $('#nodeServerList').append("<option value='" + val + "'>"); }); var pingTestHelp = "Your ping test has started.<br><br>" + "Please wait about 10 seconds for the results of the 100-ping test to return. Each time you hit enter or click the chat button " + "a new 100-ping test will be queued. Please manually clear out the words 'ping' or 'ping:host' to stop pinging and start chatting."; // Function that emits (if a socket has been established) the text in the form's input field. $('#chatForm').submit(function() { var chatString = $('#inputField').val(); if (socket) { if (chatString == 'ping') { echoTest('server'); displayMessage( pingTestHelp); } else if (chatString == 'ping:host') { echoTest('host'); displayMessage( pingTestHelp); } else { socket.emit('chat message', chatString); $('#inputField').val(''); //clear out the input field. } } else { displayMessage('Type in a short "Room" name, then click the "Connect" button.'); } return false; }); // Prevent typing in the input fields from triggering document level keyboard events. $('#inputField, #nodeServer, #roomName, #jsonCapture').on('keyup keydown keypress', function( e) { e.stopPropagation(); // stops bubbling... }); // A first message in the chat area var helpFindClientLink = ''; if (hostOrClient == 'host') { helpFindClientLink = 'You can be the host of a multi-player room from this page. '+ 'Please notice the links to the client page in the right panel below the multiplayer checkbox.</br></br>'; } else { helpFindClientLink = 'You can be a client in a multi-player room from this page. You can not be the host.</br></br>'; } var helloMessage = 'Thank you for trying the multiplayer feature.</br></br>'+ helpFindClientLink + 'To get started, type in a short "Room" name, then click the "Connect" button.</br></br>'+ 'Please note that if you do not get an immediate response from the server, it can take a little while for the Heroku node application to wake up. '+ 'If waking, give it 10 to 20 seconds before expecting a message here.</br></br>'+ 'To start over, or disconnect from the server, please reload the page.'; displayMessage( helloMessage); } function clientColor( clientName) { var colors = {'1':'yellow','2':'blue','3':'green','4':'pink','5':'orange', '6':'brown','7':'greenyellow','8':'cyan','9':'tan','0':'gray'}; var n = clientName.slice(1); var colorIndex = n - Math.trunc(n/10)*10; return colors[ colorIndex]; } function init_socket_listeners( roomName, hostOrClient) { // Listeners needed by both the client and the host. // Listen for chat from the server. socket.on('chat message', function(msg) { // Change the border color of the roomName input box depending on the // message from the node server. if (msg.includes('You have joined room')) { document.getElementById("roomName").style.borderColor = "#008080"; //Dark green. } else if (msg.includes('Sorry, there is no host')) { document.getElementById("roomName").style.borderColor = "red"; } else if (msg.includes('You are the host')) { msg += '</br></br>You can open a test <a href="indexClient.html" target="_blank">client</a> in a new tab, then drag the tab to make a new window.&nbsp;'; msg += 'Enter the same room name on the client page. Then the client mouse and keyboard events will render to the canvas of the host.'; } displayMessage( msg); }); // Once your connection succeeds, join a room. socket.on('connect', function() { // Connected. Send the room name to the server for room joining. if (hostOrClient == 'host') { // Request to be the host for that room. socket.emit('roomJoinAsHost', roomName); } else { socket.emit('roomJoin', JSON.stringify({'requestStream':chkRequestStream.checked, 'roomName':roomName})); } }); // Listen for echo response from the server. socket.on('echo-from-Server-to-Client', function( msg) { var echoTarget = msg; // Stop timer (measure the round trip). timer.stop = window.performance.now(); var elapsed_time = timer.stop - timer.start; // Add this new timing result to the array. timer.pingArray.push( elapsed_time); // The echo series STOPs here. if (timer.pingArray.length > 99) { var timeAvg = math.mean( timer.pingArray).toFixed(1); var timeSTD = math.std( timer.pingArray).toFixed(1); var timeLen = timer.pingArray.length; var timeMax = math.max( timer.pingArray).toFixed(1); var timeMin = math.min( timer.pingArray).toFixed(1); displayMessage('Echo test to '+ echoTarget +': '+ timeAvg +' ms '+ '(std='+ timeSTD + ', min='+ timeMin + ', max='+ timeMax + ', n='+ timeLen +')'); timer.pingArray = []; return; } // Ping it again (continue the series). echoTest( echoTarget); // Do this after the timer starts (don't slow it down with a write to the console.) console.log( echoTarget); }); // WebRTC Signaling. // This handles signaling from both sides of the peer-to-peer connection. socket.on('signaling message', function(msg) { // Convert it back to a usable object (parse it). var signal_message = JSON.parse(msg); // Note that signalData needs to be in a stringified form when writing to the console. //console.log("signal message from " + signal_message.from + ", to " + signal_message.to + ": " + JSON.stringify(signal_message.signalData)); // Offers and Answers if (signal_message.signalData.sdp) { //console.log('sdp in signal from host: ' + JSON.stringify(signal_message.signalData)); if (signal_message.signalData.type == 'offer') { //console.log("an offer"); handleOffer( signal_message.signalData); } else if (signal_message.signalData.type == 'answer') { //console.log("an answer"); handleAnswer( signal_message.signalData); } else { console.log("Woooooo-HoHo-Hoooooo, something is screwed up. This can't be good.") } // ICE candidates } else if (signal_message.signalData.candidate) { // handle ICE stuff. cl.rtc.pc.addIceCandidate( signal_message.signalData) .catch( function( reason) { // An error occurred, so... console.log('Error while handling ICE stuff:' + reason); }); //console.log('signaling state after handling ICE = ' + cl.rtc.pc.signalingState); } else { //No WebRTC stuff found in the signaling message. Maybe you are testing... console.log("In final else block of 'signaling message' handler.") } }); // Listeners needed by the client only. if (hostOrClient == 'client') { socket.on('your name is', function(msg) { // Put this name in the mouse and keyboard (mK) global that is used to send // state data from the client. //console.log("msg in 'your name is' = " + msg); mK.name = msg; // Put your name in this global (on the client side) for (possible) use by the WebRTC functions. newClientName = msg; // Initialize this global container for the WebRTC stuff. cl.name = newClientName; cl.rtc = new RTC({'user1':newClientName,'user2':'host'}); // Show the client's color. ctx.fillStyle = clientColor( mK.name); ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height); }); socket.on('disconnectByServer', function(msg) { //console.log('in client disconnectByServer, msg='+msg); var clientName = msg; displayMessage("This client (" + clientName + ") is being disconnected by the host."); document.getElementById("roomName").style.borderColor = "red"; // When the server gets this one, it will remove the socket. socket.emit('okDisconnectMe', clientName); // Shutdown and delete the client side of the WebRTC p2p connection. cl.rtc.shutdown(); mK.name = null; // Delay this so it takes effect after the p2p toggle finishes. window.setTimeout( function() { // Paint over the client-color square with a light gray to indicate no connection. ctx.fillStyle = '#EFEFEF'; ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height); displayMessage(""); displayMessage("Shutdown of the p2p connection for " + clientName + " has finished."); displayMessage(""); displayMessage(""); displayMessage(""); }, 100); }); socket.on('command-from-host-to-all-clients', function( msg) { // Clients (only) do something based on the message from the host. var command_message = JSON.parse( msg); var type = command_message.type; var command = command_message.command; //console.log("inside command-from-host-to-all-clients on client" + ", command=" + command); if (type == 'resize') { gW.adjustSizeOfChatDiv( command); if (command == 'normal') { videoMirror.width = 600, videoMirror.height = 600; } else { videoMirror.width = 1250, videoMirror.height = 950; } } else { console.log("I don't recognize that command; hey, I'm just saying..."); } }); } // Listeners needed by the host only. if (hostOrClient == 'host') { // (Note: this is the one place where calls to gW are made inside of hC.) // Listen for client mouse and keyboard (mk) events broadcast from the server. // StH: Server to Host socket.on('client-mK-StH-event', function(msg) { var msg_parsed = JSON.parse( msg); //console.log('State('+ msg_parsed.name +'):'+ msg_parsed.MD +','+ msg_parsed.bu +'): '+ msg_parsed.mX + "," + msg_parsed.mY); // Send this mouse-and-keyboard state to the engine. gW.updateClientState( msg_parsed.name, msg_parsed); }); // As host, create a new client in gW framework. socket.on('new-game-client', function(msg) { var msgParsed = JSON.parse(msg); var clientName = msgParsed.clientName; var streamRequested = msgParsed.requestStream; gW.createNetworkClient( clientName); // WebRTC. Start the p2p connection here (from the host) when we hear (from the server) // that a client is trying to connect to a room. // Make a global reference to this new (the most recent) client's RTC object. cl = gW.clients[clientName]; cl.rtc.user1 = 'host'; cl.rtc.user2 = clientName; cl.rtc.streamRequested = streamRequested; //console.log('inside new-game-client, cl.rtc.user2 = ' + cl.rtc.user2); //console.log('a'); // Start the WebRTC signaling exchange with the new client. // Diagnostic tools: chrome://webrtc-internals (in Chrome) and about:webrtc (in Firefox) try { openDataChannel( true); // open as the initiator createOffer(); } catch(e) { console.log("WebRTC startup: " + e); } //console.log('b'); // Someone just connected. Send the layout state to them (actually to everyone, but that // should, of course, cover the connecting user also). Delay it a bit... window.setTimeout( function() { resizeClients( gW.getChatLayoutState()); //console.log('cl.chatLayoutState = ' + gW.getChatLayoutState()); }, 300); }); socket.on('client-disconnected', function(msg) { var clientName = msg; //console.log('inside client-disconnected, clientName=' + clientName); // Null out any WebRTC references in c object (most recent connection on the host page) if it happens to be // this client. nullReferences_toRTC_on_c( clientName); // Do corresponding cleanup in gwModule. gW.deleteNetworkClient( clientName); }); socket.on('echo-from-Server-to-Host', function(msg) { // Bounce this back to server. // The msg string is the client id. socket.emit('echo-from-Host-to-Server', msg); //console.log(msg); }); socket.on('shutDown-p2p-deleteClient', function( msg) { var clientName = msg; //console.log('in host, shutDown-p2p-deleteClient, msg='+msg); // First check for the case where the host has reloaded their page and // then a client attempts to reconnect. if (gW.clients[ clientName]){ // Check if there is a puck controlled by this client. if (gW.clients[ clientName].puck) { // This will delete the puck, the client, and all the WebRTC stuff... gW.clients[ clientName].puck.deleteThisOne(); } else { // This will only delete the client and all the WebRTC stuff... //console.log('before deleteRTC_onClientAndHost, name=' + clientName); gW.deleteRTC_onClientAndHost( clientName); } } }); } } // The following two functions are exposed for external use and are called from within gwModule.js. function forceClientDisconnect( clientName) { //console.log('in forceClientDisconnect, name='+clientName); socket.emit('clientDisconnectByHost', clientName); } function resizeClients( command){ if (socket) { socket.emit('command-from-host-to-all-clients', JSON.stringify({'type':'resize', 'command':command})); } } function echoTest( hostOrServer) { // Start the timer for one echo. timer.start = window.performance.now(); // The echo series STARTs here. socket.emit('echo-from-Client-to-Server', hostOrServer); //console.log(hostOrServer); } //////////////////////////////////////////////// // Functions supporting the WebRTC connections. //////////////////////////////////////////////// var configuration = { 'iceServers': [{'urls': 'stun:stun1.l.google.com:19302'}] }; function openDataChannel( isInitiator) { cl.rtc.pc = new RTCPeerConnection( configuration); // send any ice candidates to the other peer cl.rtc.pc.onicecandidate = function (evt) { if (evt.candidate) { //console.log('inside onicecandidate, evt.candidate check'); var signal_message = {'from':cl.rtc.user1, 'to':cl.rtc.user2, 'signalData':evt.candidate}; socket.emit('signaling message', JSON.stringify( signal_message)); } }; // Host-side data channel if (isInitiator) { var dc_id = cl.rtc.user2.slice(1); var dc_options = {'id':dc_id, 'ordered':false, 'maxRetransmits':1}; var dc_label = "dc-" + cl.rtc.user2; cl.rtc.dataChannel = cl.rtc.pc.createDataChannel(dc_label, dc_options); cl.rtc.dataChannel.onmessage = function( e) { handle_RTC_message( e); }; cl.rtc.dataChannel.onopen = function( ){console.log("------ RTC DC(H) OPENED ------");}; cl.rtc.dataChannel.onclose = function( ){console.log("------ RTC DC(H) closed ------");}; cl.rtc.dataChannel.onerror = function( ){console.log("RTC DC(H) error.....");}; if (cl.rtc.streamRequested) { startVideoStream(); } //console.log('data channel A-block'); // Client-side data channel } else { // This side of the data channel gets established in response to the channel initialization // on the host side. cl.rtc.pc.ondatachannel = function(evt) { //console.log('data channel B1-block'); cl.rtc.dataChannel = evt.channel; // Must set up an onmessage handler for the clients too. cl.rtc.dataChannel.onmessage = function(e) { console.log("DC (@client) message:" + e.data); }; cl.rtc.dataChannel.onopen = function() { console.log("------ RTC DC(C) OPENED ------"); rtc_choke = false; refresh_P2P_indicator(); }; cl.rtc.dataChannel.onclose = function() { console.log("------ RTC DC(C) closed ------"); rtc_choke = true; refresh_P2P_indicator(); }; cl.rtc.dataChannel.onerror = function() { console.log("RTC DC(C) error....."); }; } //console.log('data channel B-block'); // Respond to a new track by sending the stream to the video element. cl.rtc.pc.ontrack = function (evt) { videoMirror.srcObject = evt.streams[0]; }; } //console.log('signaling state after openDataChannel = ' + cl.rtc.pc.signalingState); } function startVideoStream() { if (!videoStream) { var hostCanvas = document.getElementById('hostCanvas'); videoStream = hostCanvas.captureStream(); //60 } cl.rtc.pc.addTrack( videoStream.getVideoTracks()[0], videoStream); document.getElementById("chkStream").checked = true; videoStream.getVideoTracks()[0].enabled = true; } function setCanvasStream( newState) { if (videoStream) { if (newState == 'on') { videoStream.getVideoTracks()[0].enabled = true; } else { videoStream.getVideoTracks()[0].enabled = false; } } } function handle_RTC_message( msg) { //var user2 = Object.assign({}, cl.rtc.user2); /* var user2 = JSON.stringify(cl.rtc.user2); console.log("I am (cl.rtc.user2) = " + user2); console.log("DC ID = " + JSON.stringify(cl.rtc.dataChannel.id)); console.log("DC (@host) message: " + e.data); */ // Process mK events from the client on the other end of this peer-to-peer connection. var mK_string = msg.data; //console.log('mK_string = ' + mK_string); var mK_data = JSON.parse( mK_string); // Send this mouse-and-keyboard state to the engine. gW.updateClientState( mK_data.name, mK_data); } function createOffer() { cl.rtc.pc.createOffer() .then( function( offer) { return cl.rtc.pc.setLocalDescription( offer); }) .then( function() { var signal_message = {'from':cl.rtc.user1, 'to':cl.rtc.user2, 'signalData':cl.rtc.pc.localDescription}; socket.emit('signaling message', JSON.stringify( signal_message)); }) .catch( function(reason) { // An error occurred, so handle the failure to connect console.log('Error while creating offer:' + reason); }); //console.log('signaling state after createOffer = ' + cl.rtc.pc.signalingState); } function handleOffer( msg) { openDataChannel( false); // Open as NOT the initiator cl.rtc.pc.setRemoteDescription( msg) .then(function() { return cl.rtc.pc.createAnswer( ); }) .then(function( answer) { return cl.rtc.pc.setLocalDescription( answer); }) .then(function() { // Send the answer (localDescription) to the remote peer var signal_message = {'from':cl.rtc.user1, 'to':cl.rtc.user2, 'signalData':cl.rtc.pc.localDescription}; socket.emit('signaling message', JSON.stringify( signal_message)); }) .catch( function( reason) { console.log('Error while handling offer:' + reason); }); //console.log('signaling state after handleOffer = ' + cl.rtc.pc.signalingState); } function handleAnswer( answer) { cl.rtc.pc.setRemoteDescription( answer) .catch( function( reason) { console.log('Error while handling answer:' + reason); }); //console.log('signaling state after handleAnswer = ' + cl.rtc.pc.signalingState); } function logError( error) { console.log(error.name + ': ' + error.message); } function nullReferences_toRTC_on_c( clientName) { // Check the global "c" pointer (to the most recently connected client) to see if it happens to // be pointed at this client. //console.log('cl.rtc='+JSON.stringify( cl.rtc) + ", newClientName=" + clientName); if (cl.rtc && (cl.rtc.user2 == clientName)) { cl.rtc = new RTC({}); } } function refresh_P2P_indicator() { //console.log("fileName = " + fileName); //console.log("mK.name = " + mK.name); if (mK.name) { // Show (flood/erase the canvas with) the client's color. ctx.fillStyle = clientColor( mK.name); ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height); //console.log("rtc_choke = " + rtc_choke); //console.log("cl.rtc = " + JSON.stringify( cl.rtc)); //console.log("cl.rtc.dataChannel = " + JSON.stringify( cl.rtc.dataChannel)); if (!rtc_choke && cl.rtc && cl.rtc.dataChannel && (cl.rtc.dataChannel.readyState == 'open')) { //console.log("inside 'P2P' write block"); ctx.font = "12px Arial"; // Use dark letters for the lighter client colors. var lightColors = ['yellow', 'greenyellow', 'pink', 'cyan', 'tan']; if (lightColors.includes( clientColor( mK.name))) { ctx.fillStyle = 'black'; } else { ctx.fillStyle = 'white'; } ctx.fillText('P2P', 10, 12); } } else { // Light gray fill. ctx.fillStyle = '#EFEFEF'; ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height); } } //////////////////////////////////////////////////////////////////////////////// // Event listeners to capture mouse and keyboard (m & K) state from the non-host // clients. //////////////////////////////////////////////////////////////////////////////// function init_eventListeners_nonHostClients() { // Initialize the Mouse and Keyboard (mK) state object. // isMouseDown mK.MD = false; // mouse button number (which of the three: 0,1,2) mK.bu = 0; // mouse position in pixels: X_px, Y_px mK.mX = 5; mK.mY = 5; // Use the keyMap to define and initialize all the key states (to UP) in the // mK (mouse and keyboard state) object that is sent to the host. for (var key in keyMap) { mK[keyMap[key]] = 'U'; } for (var key in keyMap_cso) { mK_cso[keyMap_cso[key]] = 'U'; } clientCanvas = document.getElementById('connectionCanvas'); ctx = clientCanvas.getContext('2d'); videoMirror = document.getElementById('videoMirror'); // Event handlers for this network client (user input) // Inhibit the context menu that pops up when right clicking (third button). // Alternatively, could apply this only to the canvas. That way you can still // source the page. document.addEventListener("contextmenu", function(e) { e.preventDefault(); return false; }, {capture: false}); // For the client, keep these listeners on all the time so you can see the client cursor. document.addEventListener("touchmove", handleMouseOrTouchMove, {capture: false}); document.addEventListener("mousemove", handleMouseOrTouchMove, {capture: false}); document.addEventListener("mousedown", function(e) { mK.MD = true; mK.bu = e.button; //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); //if (cl.rtc && cl.rtc.dataChannel) cl.rtc.dataChannel.send( 'mouse-down event, id = ' + cl.rtc.dataChannel.id); }, {capture: false}); document.addEventListener("touchstart", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; mK.MD = true; mK.bu = 0; //Pass this first mouse position to the move handler. handleMouseOrTouchMove(e); }, {capture: false}); function handleMouseOrTouchMove(e) { // Determine if mouse or touch. if (e.clientX) { // Mouse var raw_x_px = e.clientX; var raw_y_px = e.clientY; } else if (e.touches) { // Touch screen event var raw_x_px = e.touches[0].clientX; var raw_y_px = e.touches[0].clientY; } // Note the offsets here are the same as in the handleMouseOrTouchMove handler of gwModule.js mK.mX = raw_x_px - videoMirror.getBoundingClientRect().left - 5; mK.mY = raw_y_px - videoMirror.getBoundingClientRect().top - 4; // Send the state to the server (there it will be relayed to the host client). handle_sending_mK_data( mK); //console.log("x,y=" + mK.mX + "," + mK.mY); }; function handle_sending_mK_data( mK) { // Use WebRTC datachannel if available /* */ if (cl.rtc && cl.rtc.dataChannel && (cl.rtc.dataChannel.readyState == 'open') && (rtc_choke == false)) { cl.rtc.dataChannel.send( JSON.stringify( mK)); // Otherwise use socket.io (WebSocket) } else if (socket) { socket.emit('client-mK-event', JSON.stringify( mK)); } //if (socket) socket.emit('client-mK-event', JSON.stringify( mK)); } document.addEventListener("mouseup", function(e) { if (!mK.MD) return; // Unlike for the host client, DO NOT shut down the mousemove listener. That // way we can see the mouse position even if the buttons are released. resetMouseOrFingerState(e); }, {capture: false}); document.addEventListener("touchend", function(e) { // Note: e.preventDefault() not needed here if the following canvas style is set // touch-action: none; if (!mK.MD) return; // Unlike for the host client, DO NOT shut down the touchmove listener. That // way we can see the finger position even if the buttons are released. resetMouseOrFingerState(e); }, {capture: false}); function resetMouseOrFingerState(e) { mK.MD = false; mK.bu = null; handle_sending_mK_data( mK); } document.addEventListener("keydown", function(e) { // This allows the spacebar to be used for the puck shields. if (keyMap[e.keyCode] == 'sp') { // Inhibit page scrolling that results from using the spacebar. e.preventDefault(); // The following is necessary in Firefox to avoid the spacebar from re-clicking // page controls (like the demo buttons) if they have focus. if (document.activeElement != document.body) document.activeElement.blur(); } //console.log(e.keyCode + "(down)=" + String.fromCharCode(e.keyCode)); if (e.keyCode in keyMap_cso) { //console.log("keyMap value = " + keyMap_cso[e.keyCode]); if (mK_cso[keyMap_cso[e.keyCode]] == 'U') { // Set the key to DOWN. mK_cso[keyMap_cso[e.keyCode]] = 'D'; } } // Toggle the p2p connection if ((mK_cso.key_p == 'D') && (mK_cso.key_shift == 'D')) { rtc_choke = !rtc_choke; refresh_P2P_indicator(); } if (e.keyCode in keyMap) { //console.log("keyMap value = " + keyMap[e.keyCode]); if (mK[keyMap[e.keyCode]] == 'U') { // Set the key to DOWN. mK[keyMap[e.keyCode]] = 'D'; handle_sending_mK_data( mK); } } }, {capture: false}); //"false" makes this fire in the bubbling phase (not capturing phase). document.addEventListener("keyup", function(e) { //console.log(e.keyCode + "(up)=" + String.fromCharCode(e.keyCode)); if (e.keyCode in keyMap) { // Set the key to UP. mK[keyMap[e.keyCode]] = 'U'; handle_sending_mK_data( mK); } if (e.keyCode in keyMap_cso) { // Set the key to UP. mK_cso[keyMap_cso[e.keyCode]] = 'U'; } }, {capture: false}); //"false" makes this fire in the bubbling phase (not capturing phase). // Event handlers for the check-boxes to the right of the video element. chkRequestStream = document.getElementById('chkRequestStream'); chkRequestStream.checked = true; chkRequestStream.addEventListener("click", function() { //console.log("chkRequestStream.checked=" + chkRequestStream.checked); }, {capture: false}); chkLocalCursor = document.getElementById('chkLocalCursor'); chkLocalCursor.checked = true; chkLocalCursor.addEventListener("click", function() { //console.log("chkLocalCursor.checked=" + chkLocalCursor.checked); //console.log('inside chkLocalCursor, cursor=' + videoMirror.style.cursor + '|'); if (chkLocalCursor.checked) { videoMirror.style.cursor = 'default'; } else { videoMirror.style.cursor = 'none'; } }, {capture: false}); } // Reveal public pointers to private functions and properties /////////////// return { //nodeServerURL: nodeServerURL, forceClientDisconnect: forceClientDisconnect, resizeClients: resizeClients, init_chatFeatures: init_chatFeatures, init_eventListeners_nonHostClients: init_eventListeners_nonHostClients, connect_and_listen: connect_and_listen, refresh_P2P_indicator: refresh_P2P_indicator, setCanvasStream: setCanvasStream, RTC: RTC }; })();