// Host and Client (hC) Module
// hostAndClient.js
   console.log('HC version 2.50');
// 12:26 PM Fri February 26, 2021
// Written by: James D. Miller

/*
Dependencies for hostAndClient.js:
   constructorsAndPrototypes.js (cP.)
   gwModule.js (gW.)
   utilities.js

hostAndClient.js communicates with server functionality in server.js running
at Heroku or on a local node server.

*/

var hC = (function() {
   "use strict";
   
   // Globals within hC. /////////////////////////////////////////////////
   
   if ( window.location.pathname.includes("client.html") ) {
      var hostOrClient = "client";
      // cl_clientSide is a global reference to the one and only client-like object on the client.html page.
      var cl_clientSide = {'name':null, 'previous_name':null};
   } else {
      // see comments in referenceToClient function
      var hostOrClient = "host";
   }
   
   var socket = null;
   var nodeServerURL, serverArray;
   var chatStyleToggle = true;
   
   var timer = {};
   timer.start = null;
   timer.end = null;
   timer.pingArray = [];
   
   var clientDeviceType;
   var clientCanvas, ctx;
   var clientCanvas_tt, ctx_tt;
   var videoMirror, videoMirrorDiv, videoStream;
   var chkRequestStream, chkLocalCursor;
   var demoRunningOnHost = "N/A";
   
   var chkTwoThumbs, btnTwoThumbs;
   var btnFullScreen;
   var chkPlayer;
   
   var myRequest;
      
   // Key values.
   var keyMap = {'16':'sh','17':'ct','18':'alt','32':'sp',  //sh:shift, ct:ctrl, sp:space
                 '49':'1', '50':'2', '51':'3', '52':'4', '53':'5', '54':'6', '55':'7', '56':'8', '57':'9',
                 '70':'f', 
                 '65':'a', '83':'s', '68':'d', '87':'w',
                 '74':'j', '75':'k', '76':'l', '73':'i', '90':'z',
                 '191':'cl'};  // cl (short for color), 191 is the question-mark key.
   // 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', '27':'key_esc', '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;
   var rtc_choke = false;
   
   // connection history and metrics
   var connHist = {};
   connHist.connectListenCounts = 0;
   
   var fileName = "hostAndClient.js";
   
   // supporting touch-screen event processing
   var ts = {};
   ts.previousTapTime = new Date().getTime();
   ts.tapCount = 1;
   ts.firstTouchPointID = null;
   
   // Pacifier (connecting status) string for connecting...
   var pacifier = {};
   
   // Switches to enable debugging...
   var db = {};
   db.rtc = false; // WebRTC debug.
   
   // A few globals (gb) that get exposure to other modules.
   var gb = {};
   gb.gameReportCounter = 0;
   gb.touchScreenUsage_sendCounter = 0;
   
   //////////////////////////////////////////////////   
   // Object prototypes
   //////////////////////////////////////////////////   
   
   function RTC( pars) {
      this.user1 = setDefault( pars.user1, null);
      this.user2 = setDefault( pars.user2, null);
      this.streamRequested = setDefault( pars.streamRequested, null);
      
      this.pc = null;
      this.dataChannel = null;
   }
   RTC.prototype.shutdown = function() {
      // Close then nullify any references to the datachannel and the p2p connection.
      if (this.dataChannel) {
         this.dataChannel.close();
      }
      if (this.pc) {
         var senders = this.pc.getSenders();
         if (senders.length >= 1) {
            this.pc.removeTrack( senders[0]);
            senders = this.pc.getSenders();
         }
         this.pc.close();
      }
      if (this.dataChannel) {
         this.dataChannel = null;
      }
      if (this.pc) {
         this.pc = null;
      }
   }
   // This method works only on the host side of the WebRTC connection. So, that's why there's a check here
   // to see if user1 is the host.
   RTC.prototype.turnVideoStreamOff = function() {
      if (this.pc && (this.user1 == 'host')) {
         var senders = this.pc.getSenders();
         if (senders.length >= 1) {
            this.pc.removeTrack( senders[0]);
         }
      }
   }
   
   //////////////////////////////////////////////////   
   // Functions supporting the socket.io connections
   //////////////////////////////////////////////////   
   
   function disableClientControls( diableMode) {
      // diableMode: true (disable it) or false
      if (diableMode) {
         $('#ConnectButton').html('Wait');
         $('#ConnectButton').prop('disabled', true);
         
         $('#chkRequestStream').prop('disabled', true);
         
         $('#twoThumbsButton').prop('disabled', true);
         $('#ChatButton').prop('disabled', true);
         
      } else {
         // Change the label from 'Wait' to 'Connect'.
         $('#ConnectButton').html('Connect');
         $('#ConnectButton').prop('disabled', false);
         // Note: the streaming checkbox opens when the p2p data-channel opens (see cl.rtc.dataChannel.onopen).
         //       the two-thumbs button opens when the room is successfully joined.
         //       the chat button opens when the room is successfully joined.
      }
   }
   
   function clearInputDefault( a) {
      if (a.defaultValue == a.value) {
         a.value = "";
      }
   }
   function restoreInputDefault( a) {
      a.value = a.defaultValue;
   }
   
   function checkForNickName( mode) {
      var cl = referenceToClient();
      var nickName = {'status':'ok', 'value':null};

      // Check the chat input field, e.g. jimbo
      var chatString   = $('#inputField').val(); // the users entry
      var nnFieldValue = $('#nickNameField').val(); // input field in the ghost-ball pool help panel
      var defaultValue = $('#inputField').prop('defaultValue'); //this is the value attribute in the html
      if (mode =='normal') {
         // New nick name in the chat input field. (Recently removed the need for the "nn:" prefix.)
         //if (chatString.includes('nn:') || chatString.includes('Nn:') || chatString.includes('nn ') || chatString.includes('Nn ')) {
         if ((chatString != "") && (chatString != defaultValue)) { 
            nickName.value = chatString; // chatString.slice(3, chatString.length);
            
            if (nickName.value.length > 10) {
               nickName.status = "too long";
               return nickName;
               
            } else {
               cl.nickName = nickName.value;
               
               // Clear out the input field where the nick name was entered.
               $('#inputField').val('');
               // Also sync the input field in the ghost-ball pool help panel.
               $('#nickNameField').val( nickName.value);
            }
         
         // Nothing new, so use the current nick name if it's there.   
         } else {
            if (hostOrClient == 'client') {
               nickName.value = cl.nickName;
               
            } else if (hostOrClient == 'host') {
               if (nnFieldValue != "") {
                  nickName.value = nnFieldValue;
                  cl.nickName = nickName.value;
               } else {
                  nickName.value = cl.nickName;
               }
            }
         }
      
      } else if ((mode == 're-connect') && cl.nickName) {
         nickName.value = cl.nickName;
      }
      return nickName;
   }
   
   /*
   Overview of the conversation between client, server, and host, for establishing the P2P connection:
   
   Host: 
      starting with the "Create" button, runs "connect_and_listen" to establish socket.io connection with server. 
      io.connect and init_socket_listeners are run
   Server: 
      on connection, listeners are initialized on the server for that connection
      server automatically sends to "connect" listener on host
   Host: 
      from "connect" listener, host sends to "roomJoin" listener on server with room name as indicated on the host's page
   Server: 
      from "roomJoin" listener, server sets up the room, adds the host to the room as host, 
      and sends to "room-joining-message" listener on host: (1) you joined room, (2) you are host
   Client (and Server): 
      Starting with the "Connect" button, the client makes a similar (like the host's above) exchange with the server to establish 
      the socket.io connection and join the room as a member (not as host).
      The server sends to the "your name is" listener on the client where the rtc object is instantiated and openDataChannel is run.
      openDataChannel( false, clientName); // client run this as NOT the initiator 
         the guts of the WebRTC is instantiated ----> new RTCPeerConnection
         onicecandidate event handler established which uses the "signaling message" listeners
         ondatachannel handler is defined (responds to data channel initiation by the host)
   Server: 
      At the end of the client connection process an additional step is done in the server's "roomJoin" listener. 
      The server sends the new client name to the "new-game-client" listener on the host. 
   Host: 
      "new-game-client" listener instantiates a new Client object where the host-side of each P2P connection, for each client, exists:
         gW.createNetworkClient(...)
      Then these two calls start the P2P connection process:
         openDataChannel( true, clientName); // host opens as the initiator
            the guts of the WebRTC is instantiated ----> new RTCPeerConnection
            onicecandidate event handler established
               handler will send ICE info to the server's "signaling message" listener, then relayed to the client's listener.
            createDataChannel: starts the datachannel on the host; client responds with its ondatachannel handler
         createOffer
            an offer is forwarded to the client using the "signaling message" listeners
   Client:
      "signaling message" listener
         handleOffer
            cl.rtc.pc.setRemoteDescription
            cl.rtc.pc.createAnswer
            cl.rtc.pc.setLocalDescription( answer);
            an answer is sent to the Host via "signaling message" listeners
   Host:
      "signaling message" listener
         handleAnswer
            cl.rtc.pc.setRemoteDescription( answer)
   */
   function connect_and_listen( mode) {
      // The host always connects in normal mode.
      // Re-connection happens only when the client is starting a stream or when the P2P connection makes a second attempt.
      if (mode == 'normal') {
         // Reset the counter when the connection is initiated from the button.
         connHist.connectListenCounts = 0;
         gb.touchScreenUsage_sendCounter = 0;
      } else if (mode == 're-connect') {
         // Add to the count so the number of retries can be limited.
         connHist.connectListenCounts += 1;
      } else if (mode == 're-connect-with-stream') {
         // Don't count the first re-connect associated with the video stream.
         mode = 're-connect';
         connHist.connectListenCounts = 0;
      }
      
      // Run some checks on the room name.
      var roomName = $('#roomName').val();
      // Gotta have something...
      if (roomName == "") {
         var buttonName = (hostOrClient == 'client') ? '"Connect"' : '"Create"';
         displayMessage('Type in a short "Room" name, then click the ' + buttonName + ' button.');
         document.getElementById("roomName").style.borderColor = "red";
         return;
      // 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)
      } else if (roomName.length > 7) {
         displayMessage('The name should have 7 characters or less.');
         document.getElementById("roomName").style.borderColor = "red";
         return;
      }
      
      // Clear the default string (nickname tip) in the chat field.
      clearInputDefault( document.getElementById('inputField'));
            
      // Check to see if there's a nickname in the chat input field.
      var nickName = checkForNickName( mode);
      if (nickName.status == 'too long') {
         displayMessage('Nicknames must 10 characters or less. Shorten the name and then try connecting again.');
         return;
      }
            
      if (hostOrClient == 'client') {
         // Disable some of the client controls to keep users from repeatedly
         // clicking the connect button.
         disableClientControls(true);
         refresh_P2P_indicator({'mode':'connecting'});
         
         // Open the connect button after 4 seconds. Sometimes there are network delays.
         // Note: most of the disabled controls open based on events. For example: the 
         // streaming checkbox opens when the p2p data-channel opens (see cl.rtc.dataChannel.onopen).
         window.setTimeout(function() {
            disableClientControls( false);
         }, 4000);
      } else if (hostOrClient == 'host') {
         displayMessage('Connecting as host. Please wait up to 20 seconds...');
      }
   
      var nodeString = $('#nodeServer').val();
      if (nodeString == "") {
         // Use one in the list as a default.
         nodeString = serverArray[0];  // [0] or [2]
         $('#nodeServer').val( nodeString);
      }
      if (nodeString.includes("heroku")) {
         var urlPrefix = "https://"
      } else {
         var urlPrefix = "http://"
      }
      nodeServerURL = urlPrefix + nodeString;
            
      // 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.
         
         var cl = referenceToClient();
         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 == 'client') {
               // Send a message to the host (via socket.io server) to shutdown RTC connections.
               if (newClientName) {
                  if (videoMirror.srcObject) videoMirror.srcObject = null;
                  // 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 (connection to the server) longer than the delay for socket.disconnect() above (to be sure the disconnect has finished).
         window.setTimeout( function() {
            // When starting a new normal connection, turn off the stream.
            if ((mode == 'normal') && (hostOrClient == 'client')) chkRequestStream.checked = false;
            
            // Here is where the socket.io client initiates it's connection to the server. The 'query' parameter
            // shows the form of the query string needed for a multi-parameter example. This is how you pass parameters
            // to the connection handler in server.js.
            if (nickName.value) {
               var nickNameString = '&nickName='+ nickName.value;
            } else {
               var nickNameString = '';
            }
            var queryString = 'mode=' + mode + '&currentName=' + cl.name + nickNameString;
            socket = io.connect( nodeServerURL, {'forceNew':true, 'query':queryString});
            
            init_socket_listeners( roomName);
            
         }, 600);
         
         // Check for P2P on the client and try again (reconnect) if needed.
         if ((hostOrClient == 'client') && (connHist.connectListenCounts < 1)) {
            if (window.navigator.userAgent.includes("Firefox")) {
               var waitBeforeCheck = 5500; // Mozilla
            } else {
               var waitBeforeCheck = 3500; // Chrome
            }
            window.setTimeout( function() {
               console.log('checking for p2p connection');
               var p2pConnection = ( (!rtc_choke) && (cl.rtc) && (cl.rtc.dataChannel) && (cl.rtc.dataChannel.readyState == 'open') );
               if ( ! p2pConnection) {
                  connect_and_listen('re-connect'); // Yes, connect_and_listen is this function.
               }
            }, waitBeforeCheck);
         }
         
      // 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.');
         document.getElementById("roomName").style.borderColor = "red";
         if (hostOrClient == 'client') refresh_P2P_indicator({'mode':'reset'});
      });
   }
   
   function displayMessage( msgText) {
      if (msgText.includes("Game Summary")) {
         gb.gameReportCounter += 1;
         var idString = " id='gR" + gb.gameReportCounter + "'";
      } else {
         var idString = "";
      }
      
      // Every other line, toggle the background shading.
      if (chatStyleToggle) {
         var styleString = "style='background: #efefef;'";
      } else {
         var styleString = "style='background: #d9d9d9;'";
      }
      
      $("#messages").prepend("<li " + styleString + idString + ">"+ msgText +"</li>");
      
      // Remove any help links on the client (because only the host has the help div).
      if (clientDeviceType) $(".helpLinkFromLB").remove();
      
      chatStyleToggle = !chatStyleToggle;
   }

   // Used for broadcasting a message to non-host players.
   function chatToNonHostPlayers( msgTxt) {
      if (socket) socket.emit('chat message but not me', msgTxt + '</br>');
   }
   
   function init_chatFeatures() {
      //secure-retreat-15768
      serverArray = ['timetocode.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 + "'>");
      });
      //$("#nodeServer").attr("value", "192.168.1.106:3000");
   
      var pingTestHelp = "Your ping test has started.<br><br>" +
                         "Please wait a few 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(); // user entry
         var defaultValue = $('#inputField').prop('defaultValue'); //this is the value attribute in the html
         if (socket) {
            // ping to server
            if (chatString == 'ping') {
               echoTest('server');
               displayMessage( pingTestHelp);
               
            // ping to host   
            } else if (chatString == 'ping:host') {
               echoTest('host');
               displayMessage( pingTestHelp);
            
            // peer-to-peer ping test...
            } else if (chatString.slice(0,8) == 'ping:p2p') {
               // example command from the host chat field: ping:pp-u15
               if (hostOrClient == 'host') {
                  displayMessage( pingTestHelp);
                  var clientName = chatString.split('-')[1];
                  if ((gW.clients[ clientName]) && (gW.clients[ clientName].rtc.dataChannel.readyState == 'open')) {
                     timer.start = window.performance.now();
                     gW.clients[ clientName].rtc.dataChannel.send( JSON.stringify( {'ping':true} ));
                  } else {
                     displayMessage('no client by that name (' + clientName + ') or no p2p connection with that client');
                  }
               } else {
                  displayMessage('P2P ping tests must start from the host.');
                  $('#inputField').val('');
               }
            
            // general command string input...
            } else if (chatString.slice(0,5) == 'cmd::') {
               // cmd::{"to":"roomNoSender","data":{"displayThis":"test string to the room excluding the sender"}}
               // cmd::{"to":"host","data":{"displayThis":"test string to the room's host"}}
               // cmd::{"to":"room","data":{"displayThis":"test string to the whole room"}}
               // cmd::{"to":"u20","data":{"displayThis":"test string to specific user"}}
               try {
                  let string = chatString.split('::')[1];
                  let messageCommand = JSON.parse( string);
                  sendSocketControlMessage(  messageCommand);
               } catch(e) {
                  displayMessage('Might be an error in your JSON. Use " not single quotes.<br>' + e);
                  console.log("Error: " + e);
               }
               
            // turn on (off) WebRTC debugging: set the db.rtc boolean   
            } else if (chatString.slice(0,6) == 'dbrtc:') {
               // dbrtc:on   dbrtc:off
               let value = chatString.split(':')[1];
               sendSocketControlMessage({"to":"room","data":{"dbrtc":value}});
               $('#inputField').val('');
               
            // all is well, just a chat message, send it out
            } else {
               if (chatString != defaultValue) {
                  socket.emit('chat message', chatString);
               } else {
                  displayMessage('Nickname tip has been cleared from the chat field. Ready to chat now.<br><br>' +
                                 'Note: an alternative way for the host to establish a nickname is to put it in the chat field before starting demos 6, 7, or 8.');
               }
               $('#inputField').val(''); //clear out the input field.
            }
         // no socket...   
         } else {
            // Note that I've grayed out the unconnected (no socket) chat button on both the client and host pages so
            // this little block will not run anymore.
            var buttonName = (hostOrClient == 'client') ? '"Connect"' : '"Create"';
            displayMessage('Type in a short "Room" name, then click the ' + buttonName + ' 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 helloMessage, helloMessageA;
      
      if (hostOrClient == 'host') {
         helloMessage = '' +
         'From this page you can host a multiplayer room.</br></br>'+
         
         'To get started, type a short room name into the red box, then click the "Create" button.</br></br>'+
         
         'Optionally, provide a nickname in the chat field before clicking the "Create" button or starting a game.</br></br>' +
         
         'Use the "m" key (or click the "Multiplayer" checkbox) to toggle between this chat panel (where leaderboard reports are presented) and the ' + 
         "<a onclick= \" $('#chkMultiplayer').trigger('click'); \">help panel</a>. " + 
         'Doing so will not disable connections.</br></br>'+
         
         'Please notice the links to the <a href="client.html" target="_blank">client page</a>, in the right panel, below the "Friendly fire" checkbox. '+
         'You can also get to the client page from the menu in the upper-left corner.</br></br>'+
         
         'When setting up the room as host, you might 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 confimation message in this chat area.</br></br>'+
         
         'To start over, or disconnect from the server, please reload the page.';
         
      } else {
         if (clientDeviceType == 'mobile') {
            helloMessageA = 'This is the mobile client page for multiplayer. This leads to the Two Thumbs interface. '+
                            "(Note that there are additional options with the <a href='client.html'>desktop</a> client.) </br></br>";
            window.resizeTo(475,750);   // 475,750
         } else {
            helloMessageA = 'This is the desktop client page for multiplayer. '+
                            "(Note that the <a href='client.html?m'>mobile</a> version of this client page offers the most straightforward path to the Two Thumbs interface.) </br></br>";
            window.resizeTo(1240,750);  // 1240,750
         }
         
         helloMessage = helloMessageA +
         
         'From here you can be a client in a multiplayer room. The room must be started (hosted) from the main www.timetocode.org page. '+
         'Generally, a separate computer is used for hosting. For testing, the host and multiple clients can be run in separate windows on the same computer.</br></br>'+
         
         'To connect as a client, enter the room name (provided to you by the host), into the red box here, then click the "Connect" button. '+ 
         'Optionally, provide a nickname in the chat field before clicking the "Connect" button.</br></br>' +
         
         'To start over, or disconnect from the server, please reload the page.';
      }
      displayMessage( helloMessage);
   }
   
   function clientColor( clientName) {
      var colors = cP.Client.colors;
      var n = clientName.slice(1);
      var colorIndex = n - Math.trunc(n/10)*10;
      return colors[ colorIndex];
   }

   function init_socket_listeners( roomName) {
      // Listeners needed by both the client and the host.
      
      // Listen for chat being forwarded by the server.
      socket.on('chat message', function(msg) {
         displayMessage( msg);
      });
      
      // Change the border color of the roomName input box depending on the 
      // message from the node server. And add additional info to the message.
      socket.on('room-joining-message', function( msg_string) {
         debug( db.rtc,'inside room-joining-message listener');
         var msg_object = JSON.parse( msg_string);
         var msg = msg_object.message;
         
         var cl = referenceToClient();
         
         if (msg.includes('You have joined room')) {
            // Some visual indicators that the connection succeeded.
            document.getElementById("roomName").style.borderColor = "#008080"; //Dark green.
            $('#ChatButton').prop('disabled', false);
            
            // If the names are the same, this indicates the network client has rejoined with a video stream.
            if (hostOrClient == 'client') {
               $('#twoThumbsButton').prop('disabled', false);
            
               // If the name is the same, that indicates a reconnection. Either the client has
               // requested a video stream or is making a second attempt at a P2P connection.
               if (cl.name == cl.previous_name) {
                  if (cl.nickName) {
                     var nNstring = ' (' + cl.nickName + ').';
                  } else {
                     var nNstring = '.';
                  }
                  var nameString = cl.name + nNstring;
                  
                  // Adjust the reconnection message for P2P and streaming attempts. Wait for the connection process to finish.
                  if (window.navigator.userAgent.includes("Firefox")) {
                     var waitBeforeCheck = 4500; // Mozilla
                  } else {
                     var waitBeforeCheck = 2000; // Chrome
                  }
                  window.setTimeout( function() {
                     var p2pConnection = ( (!rtc_choke) && (cl.rtc) && (cl.rtc.dataChannel) && (cl.rtc.dataChannel.readyState == 'open') );
                     if (p2pConnection) {
                        if (chkRequestStream.checked) { 
                           msg = 'You have reconnected with a video stream. Your name is still ' + nameString;
                        } else {
                           msg = 'A P2P connection has been established. Your name is still ' + nameString;
                        }
                     // P2P attempt failed
                     } else {
                        if (chkRequestStream.checked) { 
                           msg = 'You attempted to reconnected with a video stream. However, the needed P2P connection could not be established. Your name is still ' + nameString;
                        } else {
                           msg = 'An attempt to upgrade your connection from socket.io to peer-to-peer (P2P) has not succeeded. Your name is still ' + nameString + ' ' +
                                 'You may wish to simply try the "Connect" button again... ' + 
                                 '<br><br>' +
                                 'All the demos work well with a socket.io connection, but there is a little more lag and no streaming option. ' +
                                 '<br><br>' +
                                 'Difficulty establishing a P2P connection can be related to your browser or network conditions. ' +
                                 'As an alternative, you may wish to try running your own local node server as described in the "Installation of a node server" section ' +
                                 'on the <a href="multiplayer.html" target="_blank">Multiplayer</a> page.';
                        }
                     }
                     displayMessage( msg);
                  }, waitBeforeCheck);
               
               // Normal non-host client connection (not a reconnect).
               } else {
                  if (clientDeviceType == 'mobile') {
                     // Let the host know this (pure Two-Thumbs) so the client cursor can be inhibited.
                     var control_message = {'from':cl.name, 'to':'host', 'data':{'clientDeviceType':'mobile'} };
                     socket.emit('control message', JSON.stringify( control_message));
                     
                     msg += ''+
                     "</br></br>"+
                                       
                     "Touch the <strong>Two Thumbs</strong> button to start the virtual game-pad interface. This requires line-of-sight to the host's monitor. "+
                     "If you don't have line-of-sight, you can start up a second client (in desktop mode) and stream to it.</br>";
                  } else {
                     msg += ''+
                     "</br></br>"+
                     "You are in <strong>normal desktop</strong> mode. Your mouse and keyboard events get sent to the host. You must have direct visual access to the host's monitor."+
                     "</br></br>"+
                     
                     "Two other options:</br></br>"+
                     
                     "<strong>Stream:</strong> This is like normal mode, but the host's canvas is rendered in the video element here. "+
                     "So you can play out-of-sight of the host's monitor, in a separate room, city, country...</br></br>"+
                     
                     "<strong>Two Thumbs:</strong> touch-screen interface for your phone. Similar to normal mode, this requires line-of-sight to the host's monitor. "+
                     "However, you can start up a second client (on a second device) and stream to it if you don't have line-of-sight.</br>";
                  }
               } 
            
            } else if (hostOrClient == 'host') {
               cl.nameFromServer = msg_object.userName;
            }
         
         // Client might get this warning...
         } else if (msg.includes('Sorry, there is no host')) {
            document.getElementById("roomName").style.borderColor = "red";
            refresh_P2P_indicator({'mode':'reset'});
         
         // A candidate host might get this warning... 
         } else if (msg.includes('Sorry, there is already a host')) {
            document.getElementById("roomName").style.borderColor = "red";
         
         // Additional instructions for the new host. This room-joining-message event will have to be triggered a second time to get this message to the host after
         // the "You have joined room" message above.
         } else if (msg.includes('You are the host')) {
            var openWindowString = '"' + "window.open('client.html', '_blank', 'width=1240, height=750') " + '"';
            msg += ' Your name is ' + cl.nameString() + '. ' +
            "</br></br>"+
            'You can still establish or change the nickname for the host. Put it in the chat field '+
            'before running the game demos 6, 7 or 8 (do not submit as chat). This nickname is used in reports to the leaderboard.'+
            '</br></br>'+
            "You can open a test <a href='#' onClick=" + openWindowString + "title='Open a client page in a new window.'>client</a> in a new window. "+
            "Connect the client using the same room name you established here as the host. Then the client mouse and keyboard events will be transmitted to the canvas of the host.";
         }
         displayMessage( msg);
      });
      
      // Once your connection succeeds, join a room.
      socket.on('connect', function() {
         debug( db.rtc,'inside connect listener');
         
         if (hostOrClient == 'host') {
            socket.emit('roomJoin', JSON.stringify({'hostOrClient':hostOrClient,'roomName':roomName}));
            
         } else if (hostOrClient == 'client') {
            socket.emit('roomJoin', JSON.stringify({'hostOrClient':hostOrClient,'roomName':roomName,
                                                    'player':chkPlayer.checked,
                                                    'requestStream':chkRequestStream.checked}));
         }
      });
      
      // 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) {
            displayMessage( echoReport( echoTarget));
            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);
         
         if (signal_message.from == 'host') {
            var clientName = signal_message.to;
         } else {
            var clientName = signal_message.from;
         }
         var cl = referenceToClient( clientName);
         
         // Note that signalData needs to be in a stringified form when writing to the console.
         debug( db.rtc,"signal message from " + signal_message.from + ", to " + signal_message.to + ": " + JSON.stringify( signal_message.signalData));
         
         // Offers and Answers
         if (signal_message.signalData.sdp) {
            if (signal_message.signalData.type == 'offer') {
               debug( db.rtc,"an offer");
               handleOffer( clientName, signal_message.signalData);
               
            } else if (signal_message.signalData.type == 'answer') {
               debug( db.rtc,"an answer");
               handleAnswer( clientName, signal_message.signalData);
               
            } else {
               console.log("Woooooo-HoHo-Hoooooo, this can't be good.");
            }
         
         // ICE candidates
         } else if (signal_message.signalData.candidate) {
            // handle ICE stuff.
            cl.rtc.pc.addIceCandidate( signal_message.signalData)
            .then( function() {
               debug( db.rtc,'signaling state after handling ICE candidate = ' + cl.rtc.pc.signalingState);
            })
            .catch( function( reason) {
               // An error occurred, so...
               console.log('Error while handling ICE candidate:' + reason);
            });
            
            
         } else {
            //No WebRTC stuff found in the signaling message. Maybe you are testing...
            console.log("No WebRTC stuff found in the signaling message. This is the final else block of 'signaling message' listener.");
         }
      });
      
      socket.on('control message', function( msg) {
         // General receiver of control messages. This can be used by either the host or a client to
         // receive messages from anyone. Note that the server directs these messages according to the following
         // message.to values: 'host', 'room', 'roomNoSender', or a specific user name like 'u15'.
         
         // (Refer to the handler for command-from-host-to-all-clients for a similar but more specific approach to
         // command processing.)
         
         // Convert the raw msg back to a usable object (parse it).
         var message = JSON.parse( msg);
         
         // Control actions allowed on both the host and client pages.
         if (message.data['displayThis']) {
            displayMessage( message.data['displayThis']);
            
         } else if (message.data['dbrtc']) {
            displayMessage( "RTC debug setting = " + message.data['dbrtc']);
            if (message.data['dbrtc'] == "on") {
               db.rtc = true;
            } else {
               db.rtc = false;
            }
         }
         
         // Control actions allowed only on the host's page.
         // Note the use of gW.clients here. This is only available on the host device.
         if (hostOrClient == 'host') {
            if (message.data['videoStream'] == 'off') {
               gW.clients[ message.from].rtc.turnVideoStreamOff();
               
            } else if (message.data['fullScreen'] == 'off') {
               console.log('full screen requested off by client');
               // I played around with trying to do something here, but the browsers fullscreen API requires that
               // a change starts with a gesture. The error: '...API can only be initiated by a user gesture.'
               
            } else if (message.data['clientDeviceType'] == 'mobile') {
               // Attribute to inhibit client cursor.
               gW.clients[ message.from].deviceType = 'mobile';
               console.log('client ' + message.from + ' is in mobile mode');
               
            } else if (message.data['puckPopped']) {
               if (message.data['puckPopped'].value == 'probeAtHost') {
                  // Check to see if the requesting client is still in the clients object and still has a puck.
                  if ((message.from in gW.clients) && (gW.clients[ message.from].puck)) {
                     var puckPopped = false;
                  } else {
                     var puckPopped = true;
                  }
                  // Send reply message back to the client.
                  var control_message = {'from':'host', 'to':message.from, 'data':{'puckPopped':{'value':puckPopped}} };
                  sendSocketControlMessage( control_message);
                  
                  // Sync the gun and jet angles, i.e. send angles out to the clients.
                  if (gW.clients[ message.from]) gW.clients[ message.from].gunAngleFromHost(0, true);
                  if (gW.clients[ message.from]) gW.clients[ message.from].jetAngleFromHost();
               }
               
            } else if (message.data['twoThumbsEnabled']) {
               if (gW.clients[ message.from]) gW.clients[ message.from].twoThumbsEnabled = message.data['twoThumbsEnabled'].value;
            
            } else if (message.data['touchScreenUsage']) {
               if (gW.clients[ message.from]) gW.clients[ message.from].touchScreenUsage = message.data['touchScreenUsage'].value;
               
            } else if (message.data['androidDebug']) {
               gW.messages['lowHelp'].newMessage( message.data['androidDebug'].debugString, 10.0);
            }
            
         // Control actions allowed only the client page.
         } else if (hostOrClient == 'client') {
            if (message.data['canvasResize']) {
               console.log("command to resize canvas: " + message.data['canvasResize'].width + ", " + message.data['canvasResize'].height);
               videoMirror.width = message.data.canvasResize.width; 
               videoMirror.height = message.data.canvasResize.height;
               
               demoRunningOnHost = message.data['demoVersion'];
               
            } else if (message.data['gunAngle']) {
               twoThumbs.processGunAngleFromHost( message.data);
               
            } else if (message.data['jetAngle']) {
               twoThumbs.processJetAngleFromHost( message.data);
               
            } else if (message.data['puckPopped']) {
               twoThumbs.setPuckPopped( message.data['puckPopped'].value);
               // Indicate this info by updating the client-color rectangle on the TwoThumbs interface.
               twoThumbs.colorClientRect( clientColor( message.to));
            }
         }
      });
      
      // Listeners needed by the client only.

      if (hostOrClient == 'client') {  
         socket.on('your name is', function( msg) {
            var message = JSON.parse( msg);
            
            var name = setDefault( message.name, null);
            // Note: not (yet) using the nickName that comes back from the socket.io server.
            // cl_clientSide.nickName gets set for the client on the front end of the connection process.
            var nickName = setDefault( message.nickName, null);
            
            // Put this name in the mouse and keyboard (mK) global that is used to send
            // state data from the client.
            mK.name = name;
            
            // Put your name in this global (on the client side) for (possible) use by the WebRTC functions.
            newClientName = name;
            
            // Before updating cl_clientSide.name with the new client name, store it's current value in previous_name.
            cl_clientSide.previous_name = cl_clientSide.name;
            cl_clientSide.name = newClientName;
            
            debug( db.rtc,'inside "your name is", names: current='+cl_clientSide.name+ ', previous='+ cl_clientSide.previous_name +', nick='+nickName);
            
            // Initialize rtc for the client side of the p2p connection.
            cl_clientSide.rtc = new RTC({'user1':newClientName,'user2':'host'});
            openDataChannel( false); // Open as NOT the initiator
         });
         
         socket.on('disconnectByServer', function( msg) {
            debug( db.rtc,'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_clientSide.rtc.shutdown();
            initialize_mK();
            //mK = {};
            
            // Delay this so it takes effect after the p2p toggle finishes.
            window.setTimeout( function() {
               displayMessage("");
               displayMessage("Shutdown of the connection for " + clientName + " has finished.");
               displayMessage("");
               displayMessage("");
               displayMessage("");
               refresh_P2P_indicator({'mode':'disconnected'});
            }, 100);
         });
         
         // Refer to the "control message" handler for a more general approach to command processing. Below is a
         // specific host-to-all-clients approach.
         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;
            
            if (type == 'resize') {
               if (clientDeviceType == 'mobile') command = 'mobile';
               gW.adjustSizeOfChatDiv( command);
               
               if (command == 'normal') {
                  videoMirror.width = 600, videoMirror.height = 600;
               } else {
                  videoMirror.width = 1250, videoMirror.height = 950;
               }
            } else {
               console.log("no match in command-from-host-to-all-clients handler");
            }
         });
      }
      
      // 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 mk_data = JSON.parse( msg);
            // Send this mouse-and-keyboard state to the engine.
            gW.updateClientState( mk_data.name, mk_data);
         });
         
         // As host, create a new client in gW framework.
         socket.on('new-game-client', function(msg) {
            var msgParsed = JSON.parse(msg);
            
            var streamRequested = msgParsed.requestStream;
            
            var clientName = msgParsed.clientName;
            var player     = msgParsed.player;
            var nickName   = msgParsed.nickName;
            
            gW.createNetworkClient({'clientName':clientName, 'player':player, 'nickName':nickName});
            
            // 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.
            var cl_hostSide = gW.clients[ clientName];
            cl_hostSide.rtc.user1 = 'host';
            cl_hostSide.rtc.user2 = clientName;
            cl_hostSide.rtc.streamRequested = streamRequested;
            debug( db.rtc,'in new-game-client, cl_hostSide.rtc.user2 = ' + cl_hostSide.rtc.user2);
            
            // Start the WebRTC signaling exchange with the new client.
            // Diagnostic tools: chrome://webrtc-internals (in Chrome) and about:webrtc (in Firefox)
            try {
               openDataChannel(true, clientName); // open as the initiator
               debug( db.rtc,'data channel initiated');
               createOffer( clientName);
            } catch(e) {
               console.log("WebRTC startup: " + e);
            }
            
            // Someone just connected. Send the host's layout state to them (actually to everyone, but that
            // should, of course, cover the connecting user also). Delay it a bit...
            window.setTimeout( function() {
               // Adjust client chat panel and canvas to match host (normal or small chat).
               resizeClients( gW.getChatLayoutState());
               // Adjust client canvas to match specific custom dimensions of the host's canvas.
               gW.setClientCanvasToMatchHost();
            }, 300);
         });
         
         socket.on('client-disconnected', function(msg) {
            var clientName = msg;   
            debug( db.rtc,'in client-disconnected, clientName=' + clientName);
            
            nullReferences_toRTC_on_cl( 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);
         });
         
         socket.on('shutDown-p2p-deleteClient', function( msg) {
            debug( db.rtc,'in shutDown-p2p-deleteClient');
            var clientName = msg;
            // First check for the case where the host has reloaded their page and 
            // then a client attempts to reconnect. In that case the clients map will be empty and
            // this clientName won't be found in there.
            if (gW.clients[ clientName]) {
               // Check for a puck controlled by this client. Delete it first.
               if (gW.clients[ clientName].puck) gW.clients[ clientName].puck.deleteThisOne({});
               // Then start shutting down the WebRTC connection.
               gW.deleteRTC_onClientAndHost( clientName);
            }
         });
      }
   } // end of init_socket_listeners
   
   
   function forceClientDisconnect( clientName) {
      debug( db.rtc,'in forceClientDisconnect');
      socket.emit('clientDisconnectByHost', clientName);
   }
   function resizeClients( command) {
      if (socket) {
         socket.emit('command-from-host-to-all-clients', JSON.stringify({'type':'resize', 'command':command}));
      }
   }
   function sendSocketControlMessage( message) {
      // This is received and distributed at the server in its 'control message' handler. Then
      // received and processed at the host or client in their 'control message' handler.
      if (socket) {
         socket.emit('control message', JSON.stringify( message));
      }
   }

   
   ////////////////////////////////////////////////
   // Functions supporting the WebRTC connections.
   ////////////////////////////////////////////////
   
   var configuration = { 'iceServers': [{'urls': 'stun:stun1.l.google.com:19302'}] };
   
   function openDataChannel( isInitiator, clientName = 'N/A') {
      // On the client page: cl refers to the one and only object that holds name info and the RTC object for that client.
      // On the   host page: cl refers to the named client, of possibly many client instances on the host, in the clients array.
      // referenceToClient switches cl to make the appropriate reference depending on the page context.
      var cl = referenceToClient( clientName);
      cl.rtc.pc = new RTCPeerConnection( configuration);
      
      cl.rtc.pc.onicecandidate = function (evt) {
         if (evt.candidate) {
            // send any ice candidates to the other peer
            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) {
         debug( db.rtc,'host is setting up datachannel...');
         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) {
            var objFromClient = JSON.parse( e.data);   
            
            // Ping to client test...
            if (objFromClient['ping']) {
               timer.stop = window.performance.now();
               var elapsed_time = timer.stop - timer.start;
               timer.pingArray.push( elapsed_time);
         
               if (timer.pingArray.length < 100) {
                  timer.start = window.performance.now();
                  gW.clients[this.user2].rtc.dataChannel.send( JSON.stringify( {'ping':true} ));
               } else {
                  displayMessage( echoReport( this.user2));
                  timer.pingArray = [];
               }
               
            } else {
               handle_RTC_message( objFromClient);
            }
         }.bind({'user2':cl.rtc.user2});  // bind object to "this" so it (this.user2) is available when onmessage runs.
         
         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(e) {
            if (e.error != "OperationError: Transport channel closed") console.log("RTC DC(H) error: " + e.error);
         };
         
         if (cl.rtc.streamRequested) {
            startVideoStream( clientName);
         }
      
      // Client-side data channel
      } else {
         debug( db.rtc,'client is setting up datachannel...');

         // This side of the data channel gets established in response to the channel initialization 
         // on the host side.
         cl.rtc.pc.ondatachannel = function(evt) {
            debug( db.rtc,'client response in ondatachannel handler');
            cl.rtc.dataChannel = evt.channel;
            
            // Must also set up an onmessage handler for the clients.
            cl.rtc.dataChannel.onmessage = function(e) {
               debug( db.rtc,"DC (@client) message:" + e.data);
               var objFromHost = JSON.parse( e.data);
               
               // Ping back to the host
               if (objFromHost['ping']) {
                  cl.rtc.dataChannel.send( JSON.stringify( {'ping':true} ));
               
               } else {
                  // The gun-angle info is on a 'data' key of the sent object.
                  twoThumbs.processGunAngleFromHost( objFromHost.data);
               }
            };
            
            cl.rtc.dataChannel.onopen = function() {
               console.log("------ RTC DC(C) OPENED ------");
               rtc_choke = false;
               $('#chkRequestStream').prop('disabled', false);
               refresh_P2P_indicator({'mode':'p2p', 'context':'dataChannelOpen'});
            };
            cl.rtc.dataChannel.onclose = function() {
               console.log("------ RTC DC(C) closed ------");
               rtc_choke = true;
            };
            cl.rtc.dataChannel.onerror = function(e) {
               if (e.error != "OperationError: Transport channel closed") console.log("RTC DC(C) error: " + e.error);
            };
         }
         
         // Respond to a new track by sending the stream to the video element.
         cl.rtc.pc.ontrack = function (evt) {
            videoMirror.srcObject = evt.streams[0];
         };
      }
      debug( db.rtc,'signaling state at end of openDataChannel = ' + cl.rtc.pc.signalingState);
   }
   
   // This function is used (only) by the host when someone connects and wants a stream.
   function startVideoStream( clientName) {
      var cl = referenceToClient( clientName);
      
      if (!videoStream) {
         var hostCanvas = document.getElementById('hostCanvas');
         videoStream = hostCanvas.captureStream(); //60
      }
      cl.rtc.pc.addTrack( videoStream.getVideoTracks()[0], videoStream);
      // The chkStream is on the host page only (index.html)
      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( mK_data) {
      //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);
      */
      
      // Send this mouse-and-keyboard state to the host engine.
      gW.updateClientState( mK_data.name, mK_data);
   }
   
   function createOffer( clientName) {
      var cl = referenceToClient( clientName);
      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));
         debug( db.rtc,'signaling state after createOffer = ' + cl.rtc.pc.signalingState);
      })
      .catch( function(reason) {
         console.log('Error while creating offer:' + reason);
      });
   }
   
   function handleOffer( clientName, msg) {
      var cl = referenceToClient( clientName);
      
      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));
         debug( db.rtc,'signaling state after handleOffer = ' + cl.rtc.pc.signalingState);
      })
      .catch( function( reason) {
         console.log('Error while handling offer:' + reason);
      });
   }
   
   function handleAnswer( clientName, answer) {
      var cl = referenceToClient( clientName);
      cl.rtc.pc.setRemoteDescription( answer)
      .then( function() {
         debug( db.rtc, 'signaling state after handleAnswer = ' + cl.rtc.pc.signalingState);
      })
      .catch( function( reason) {
         console.log('Error while handling answer:' + reason);
      });
   }

   function logError( error) {
      console.log( error.name + ': ' + error.message);
   }   
   
   function nullReferences_toRTC_on_cl( clientName) {
      var cl = referenceToClient( clientName);
      // Check the global "cl" pointer (to the most recently connected client) to see if it happens to
      // be pointed at this client.
      if (cl) {
         debug( db.rtc, 'in nullReferences_toRTC_on_cl \n  cl.rtc=' + JSON.stringify( cl.rtc) + ", newClientName=" + clientName);
         if (cl.rtc && (cl.rtc.user2 == clientName)) {
            cl.rtc = new RTC({});
         }
      } else {
         // client not found in clients array; maybe already removed by a disconnection...
         //debug( db.rtc, "can't find " + clientName + " in clients");
      }
   }

   function refresh_P2P_indicator( pars) {
      var mode = setDefault( pars.mode, 'p2p');
      var context = setDefault( pars.context, null);
      
      // Stop the pacifier (note: pacifier is a global object)
      // clearInterval() method clears a timer set with the setInterval() method
      clearInterval( pacifier.intFunction);
      
      var cl = referenceToClient();
      
      // If connected, there will be a name (assigned from the server)
      if ((mode == 'p2p') && cl.name) {
         // Show (flood/erase the canvas with) the client's color.
         ctx.fillStyle = clientColor( cl.name);
         ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height);
         
         ctx.font = "12px Arial";
         // Use dark letters for the lighter client colors.
         var lightColors = cP.Client.lightColors; // an array
         if (lightColors.includes( clientColor( cl.name))) {
            ctx.fillStyle = 'black';
         } else {
            ctx.fillStyle = 'white';
         }
         
         // If the rtc choke is off and there's a data channel, display the "P2P" text.
         if (!rtc_choke && cl.rtc && cl.rtc.dataChannel && (cl.rtc.dataChannel.readyState == 'open')) {
            ctx.fillText('P2P', 10, 12);
         } else {
            ctx.fillText('socket.io', 10, 12);
         }
         
      } else if (mode == 'connecting') {
         ctx.fillStyle = 'darkgray';
         ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height);
         
         ctx.font = "12px Arial";
         ctx.fillStyle = 'white';
         ctx.fillText('CONNECTING', 10, 12);
         
         // Start the pacifier
         pacifier.string = '';
         pacifier.intFunction = setInterval( function() { 
            pacifier.string += '--';
            ctx.fillText( pacifier.string, 95, 12);
         }, 200);
         
      } else if (mode == 'reset') {
         // Light gray fill.
         ctx.fillStyle = '#EFEFEF';
         ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height);
         
      } else if (mode == 'disconnected') {
         ctx.fillStyle = 'darkgray';
         ctx.fillRect(0, 0, clientCanvas.width, clientCanvas.height);
         
         ctx.font = "12px Arial";
         ctx.fillStyle = 'white';
         ctx.fillText('DISCONNECTED', 10, 12);
      }
   }
   
   ////////////////////////////////////////////////////////////////////////////////
   // Functions supporting canvas animation
   ////////////////////////////////////////////////////////////////////////////////
   
   // Currently not using this steady animation loop approach. Instead, update the canvas 
   // as input events get fired. Refer to the methods in the TwoThumbs class.
   
   /*
   
   function canvasLoop( timeStamp_ms) {
      updateCanvas();
      
      myRequest = window.requestAnimationFrame( canvasLoop);
   }
   
   function updateCanvas() {
      // Clear the canvas (from one corner to the other)
      if (ctx_tt.globalCompositeOperation == 'screen') {
         ctx_tt.clearRect(0,0, clientCanvas_tt.width, clientCanvas_tt.height);
      } else {
         ctx_tt.fillStyle = 'blue';
         ctx_tt.fillRect(clientCanvas_tt.width/8, clientCanvas_tt.width/8, clientCanvas_tt.width/4, clientCanvas_tt.height/4);
      }
      
      if (twoThumbs.getEnabled()) {
         // Draw the two-thumb state
         if (cl.name) {
            var circleColor = clientColor( cl.name);
         } else {
            var circleColor = 'white';
         }
         this.drawCircle( ctx_tt, {'x':mK.mX, 'y':mK.mY}, {'fillColor': circleColor} );
      }
   }
   
   function startAnimation() {
      // Only start a game loop if there is no game loop running.
      if (myRequest === null) {
         // Start the canvas loop.
         myRequest = window.requestAnimationFrame( canvasLoop);
      }
   }
   
   function stopAnimation() {
      window.cancelAnimationFrame( myRequest);
      myRequest = null;
   }
   
   */
   
   
   ////////////////////////////////////////////////////////////////////////////////
   // Misc functions
   ////////////////////////////////////////////////////////////////////////////////
   
   /*
   It's important, as multiple players connect, to use referenceToClient, 
   especially in the context of the host's page, to set references within 
   the local scope of the functions and callbacks used below. Global scope, 
   on the host page, could result in references changing, as a new player 
   connects, before the ongoing asynchronous connection process completes. 

   When referenceToClient is called without a parameter, it returns a 
   reference to either gW.clients['local'] (on the host's page) or 
   cl_clientSide (on the client page). If a client name is provided (and on 
   the host page) this points at the particular client in the clients 
   array.
   */
   function referenceToClient( clientName = 'local') {
      if (hostOrClient == 'client') {
         // a reference to the one and only client-similar object on the client page
         var clientRef = cl_clientSide;
      } else if (hostOrClient == 'host') {
         // a reference to one of the clients on the host page
         var clientRef = gW.clients[ clientName];
      }
      return clientRef;
   }
   
   function init_nonHostClients() {
      
      // Get the URL query string. Discard everything after the "&".
      var queryStringInURL = window.location.search.split("&")[0];
      // Take everything after the ? and set a module (hC) level global, clientDeviceType, that will
      // be used in restricting features for a simplified mobile version of the client page.
      var queryStringValue = queryStringInURL.slice(1);
      if (queryStringValue == "m") {
         clientDeviceType = "mobile";
      } else {
         clientDeviceType = "desktop";
      }
      
      if (clientDeviceType == 'mobile') {
         gW.adjustSizeOfChatDiv('mobile');
      } else {
         gW.adjustSizeOfChatDiv('normal');          
      }
      
      init_eventListeners_nonHostClients();
      init_chatFeatures();
      twoThumbs.initializeModule( clientCanvas_tt, ctx_tt, videoMirror, mK, cl_clientSide);
      
      if (clientDeviceType == 'mobile') {
         // Resize (reduce) the button
         $("#twoThumbsButton").css("height", "28px");
         // Move it
         $('#divForTwoThumbsMobile').append( $('#twoThumbsButton') );
         
         // Hide the video streaming element.
         videoMirror.setAttribute("hidden", null);
         
         // Hide controls
         $("#nodeServerDiv").hide();  
         $("#playerAndCursor").hide();  
         $("#streamAndFullscreen").hide();  
         $("#twoThumbsButtonDiv").hide();       

         // page title
         document.title = "S&P mobile client";
      }
      
      // Hide the div that covers the mess (while elements are moving)
      $("#blankWhiteDiv").hide();
   }
   
   function openFullscreen( elem) {
      if (elem.requestFullscreen) {
         console.log("fullscreen - normal");
         elem.requestFullscreen();
      } else if (elem.mozRequestFullScreen) { /* Firefox */
         console.log("fullscreen - moz");
         elem.mozRequestFullScreen();
      } else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
         console.log("fullscreen - webkit");
         elem.webkitRequestFullscreen();
      } else if (elem.msRequestFullscreen) { /* IE/Edge */
         console.log("fullscreen - ms");
         elem.msRequestFullscreen();
      } else {
         console.log("openFullscreen: found no match");
      }
   }
   
   function closeFullscreen() {
      if (document.exitFullscreen) {
         document.exitFullscreen();
      } else if (document.mozCancelFullScreen) { /* Firefox */
         document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
         document.webkitExitFullscreen();
      } else if (document.msExitFullscreen) { /* IE/Edge */
         document.msExitFullscreen();
      }
   }   

   function changeFullScreenMode( targetElement, mode) {
      if (mode == 'on') {
         if (targetElement.requestFullscreen) {
             targetElement.requestFullscreen();
         } else {
            console.log("can't find requestFullscreen method on target element.");
            // try a more general call (needed for browser in WebOS on an LG TV)
            openFullscreen( targetElement);
         }
         
      } else if (mode == 'off') {
         if (document.exitFullscreen) {
            // This check for fullscreenElement keeps a call to exitFullscreen from being attempted
            // if somehow the user has already exited fullscreen mode.
            // Refer to the event handler for fullscreenechange where this statement runs: twoThumbs.changeDisplay('exit');
            if (document.fullscreenElement) {
               document.exitFullscreen()
                  .then((    ) => console.log("scripted fullscreen exit went well."))
                  .catch((err) => console.log("caught error on fullscreen exit:" + err));
            } else {
               console.log("no fullscreenElement; changeFullScreenMode ran AFTER fullscreen exit.");
               // Try a more general call.
               closeFullscreen();
            }
         } else {
            console.log("can't find exitFullscreen method.");
         }
      }
   }
   
   function handle_sending_mK_data( mK) {
      var cl = cl_clientSide;
      
      // 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));
      }
   }
   
   function debug( flag, message) {
      if (flag) console.log( message);
   }
   
   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);
   }
   function echoReport( echoTarget) {
      // Note the lowercase m on math.mean for example. These methods are from the mathjs library. See script load on index.html and client.html.
      // These are not part of the native Math (upper case M) methods.
      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);
      var reportString = 'Echo test to '+ echoTarget +': '+ timeAvg +' ms '+
          '(std='+  timeSTD +
          ', min='+ timeMin +
          ', max='+ timeMax +
          ', n='+   timeLen +')';
      return reportString;
   }
   
   ////////////////////////////////////////////////////////////////////////////////
   // Event listeners to capture mouse and keyboard (m & K) state from the non-host 
   // clients. 
   ////////////////////////////////////////////////////////////////////////////////
   
   function initialize_mK() {
      // 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; 
      // mouse wheel
      mK.mW = 'N'; // F,B, or N (forward, backward, or neutral)
      
      // 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';
      }
      // Initialize non-keyboard attributes (for the Two Thumbs interface)
      // These values are default when using the keyboard.
      mK['ScRrf'] = 0.00; // gun scope rotation rate fraction (0.00, no constant sweeping)
      mK['ScTr'] = 'U';   // gun scope trigger ('U', scope trigger is off)
      mK['jet_t'] = 1.0;  // jet throttle (1.0, jet is full throttle)
   }
   
   function init_eventListeners_nonHostClients() {
      initialize_mK();
   
      clientCanvas = document.getElementById('connectionCanvas');
      ctx = clientCanvas.getContext('2d');
      
      clientCanvas_tt = document.getElementById('twoThumbsCanvas');
      ctx_tt = clientCanvas_tt.getContext('2d');
      
      myRequest = null;

      videoMirror = document.getElementById('videoMirror');
      videoMirrorDiv = document.getElementById('divForClientCanvas');
      
      // Event handlers for this network client (user input)
      
      // For the client, keep these listeners on all the time so you can see the client cursor.
      // To avoid some default behavior on the video element, had to set up separate event handlers
      // for that element (videoMirror) and use preventDefault.
      
      // 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});
      
      
      
      clientCanvas_tt.addEventListener("touchstart", function(e) {
         e.preventDefault();
                  
         mK.MD = true;  // Mouse Down
         mK.bu = 0; // Mouse button
         
         //Pass this initial touch position to the move handler.
         handleMouseOrTouchMove( e, 'touchstart');
      }, {capture: false});   
      
      clientCanvas_tt.addEventListener("touchmove", function(e) {
         e.preventDefault();
         handleMouseOrTouchMove( e, 'touchmove');
      }, {capture: false});
      
      clientCanvas_tt.addEventListener("touchend", function( e) {
         // note: canvas style ====> touch-action: none;
         // prevent mousedown event...
         e.preventDefault();
         resetMouseOrFingerState( e);
      }, {capture: false});
      
      
      
      // This "click" handler on the parent div for the streaming video element
      // prevents click events from pausing the stream when in fullscreen mode (needed for Chrome).
      videoMirrorDiv.addEventListener("click", function(e) {
         e.preventDefault();
      }, {capture: false});
      
      videoMirror.addEventListener("touchstart", function(e) {
         e.preventDefault();
                  
         mK.MD = true;  // Mouse Down
         mK.bu = 0; // Mouse button
         
         //Pass this initial touch position to the move handler.
         handleMouseOrTouchMove( e, 'touchstart');
      }, {capture: false}); 
      
      videoMirror.addEventListener("touchmove", function(e) {
         e.preventDefault(); // prevent scrolling behavior when not in fullscreen mode
         handleMouseOrTouchMove( e, 'touchmove');
      }, {capture: false});
      
      videoMirror.addEventListener("touchend", function( e) {
         //note: videoMirror style ====> touch-action: none;
         /*
         e.preventDefault() is needed here in the final touch event to 
         prevent mouse events from firing after touch events. This is especially 
         important since screenFromRaw_2d_px does a stretching operation for 
         touch screens and if a mouse event fires, the cursor will appear to jump 
         around. Here's the usual order that the events fire... 
         
         touchstart
         touchmove
         touchend
         ----------
         mousemove
         mousedown
         mouseup
         click 
         
         */         
         e.preventDefault();
         resetMouseOrFingerState( e);
      }, {capture: false});
      
      videoMirror.addEventListener("mousedown", function(e) {  
         // If using the videoMirror with a mouse
         mK.MD = true; // Mouse Down
         mK.bu = e.button; // Mouse button
         
         //Pass this initial mouse position to the move handler.
         handleMouseOrTouchMove( e, 'mousedown');
      }, {capture: false});
      
      videoMirror.addEventListener("mousemove", function(e) {
         handleMouseOrTouchMove( e, 'mousemove');
      }, {capture: false});
         
      videoMirror.addEventListener("mouseup", function( e) {
         if (!mK.MD) return;
         // Unlike what could be done 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});
      

      function tripleTap() {
         var now = new Date().getTime();
         var timesince = now - ts.previousTapTime;
         ts.previousTapTime = now;
         // A good short double tap
         if ((timesince > 0) && (timesince < 300)) {
            ts.tapCount += 1;
            // That's a triple.
            if (ts.tapCount == 3) {
               ts.tapCount = 1;
               return true;
            // Nice double, but not a triple.
            } else {
               return false;
            }
         // Too much time has passed, so reset.
         } else {
            ts.tapCount = 1;
            return false;
         }
      }
      
      function handleMouseOrTouchMove( e, fromListener) {
         var cl = referenceToClient();
         
         // Process mousedown, mousemove, touchstart, and touchmove events.
         if (twoThumbs.getEnabled()) {
            var touchPoints_2d_px = [];
            
            // Determine event type
            // Mouse (single contact point)
            if ((e.clientX || (e.clientX === 0)) && (mK.MD == true)) {
               touchPoints_2d_px[0] = screenFromRaw_2d_px( clientCanvas_tt, new cP.Vec2D( e.clientX, e.clientY), {'demoRunningOnHost':demoRunningOnHost});
            
            // Touch screen (possibly multiple contact points)
            } else if (e.touches) {
               /*
               // Tried this but can't. Must start with a gesture on the host.
               // Use 4-finger touch to toggle fullscreen on the host.
               if ((e.touches.length == 4) && (fromListener != 'touchmove')) {
                  var control_message = {'from':cl.name, 'to':'host', 'data':{'fullScreen':'off'} };
                  socket.emit('control message', JSON.stringify( control_message));
               }
               */
               for (var i = 0, len = e.touches.length; i < len; i++) {
                  touchPoints_2d_px[i] = screenFromRaw_2d_px( clientCanvas_tt, new cP.Vec2D( e.touches[i].clientX, e.touches[i].clientY), {'demoRunningOnHost':demoRunningOnHost});
               }
            }
            // Interpret the touch and mouse events using the twoThumbs interface.
            twoThumbs.processMultiTouch( touchPoints_2d_px);
         
         // Non-twoThumbs
         } else {
            // If NOT in twoThumbs AND in the mobile version of this page, don't send mouse or touch data.
            if (clientDeviceType == "mobile") return;
            
            // Determine event type
            // Mouse
            if (e.clientX || (e.clientX === 0)) {
               var inputDevice = "mouse";
               var raw_x_px = e.clientX;
               var raw_y_px = e.clientY;
            
            // Translate touch-screen events (non-twoThumbs) into keyboard data for sending to the host 
            // for ghost-ball pool shots.
            // (see also resetMouseOrFingerState)
            } else if (e.touches) {
               if ((e.touches.length == 1) && (fromListener != 'touchmove')) {
                  // Save the first touch id. This is needed to interpret Firefox touch ids because they keep incrementing. For Chrome this first touch id is
                  // always 0 (zero). So simply subtracting this from any raw touch id gives the needed relative touch id (for both Chrome and Firefox): 0, 1, 2, 3...
                  ts.firstTouchPointID = e.touches[0].identifier;
               }
               
               var inputDevice = "touchScreen";
               var touchID = e.changedTouches[0].identifier - ts.firstTouchPointID;  // the relative touch id: 0,1,2,3...
               
               // Toggle ball-in-hand state when triple-tap the first touch point. This is like holding
               // down the control key when using the keyboard.
               if ( (fromListener != 'touchmove') && (touchID == "0") ) {
                  if ( tripleTap() ) {
                     sendSocketControlMessage( {'from':cl.name, 'to':'host', 'data':{'touchScreenUsage':{'value':true}} } );
                     if (mK['ct'] == 'D') {
                        mK['ct'] = 'U';
                     } else {
                        mK['ct'] = 'D';
                     }
                  }
               }
               
               // Do nothing as the first point is started. 
               // Cue ball shoots when the first touch point is lifted.
               if (touchID == "0") {
               
               // Lock in a new value for cue-ball speed when second touch point is started.   
               } else if (touchID == "1") {
                  mK['z'] = 'D';
               
               // Nothing...
               } else if (touchID == "2") {
                              
               // Restart the game when forth touch point is started.
               } else if (touchID == "3") {
                  mK['3'] = 'D';
               }
               
               // Only consider the first touch point for establishing cursor position.
               var raw_x_px = e.touches[0].clientX;
               var raw_y_px = e.touches[0].clientY;
            }
            
            // Convert the raw mouse position into coordinated relative to the corner of the imaging element.
            var screen_2d_px = screenFromRaw_2d_px( videoMirror, new cP.Vec2D( raw_x_px, raw_y_px), {'inputDevice':inputDevice, 'demoRunningOnHost':demoRunningOnHost});
            // Send the state to the server (there it will be relayed to the host client).
            mK.mX = parseFloat(  ( screen_2d_px.x ).toFixed(2)  ); // crop down to 2 decimal points before sending over the network
            mK.mY = parseFloat(  ( screen_2d_px.y ).toFixed(2)  );
            // Delay the mK data 0.1 seconds so that the touchScreenUsage control message, see above, is certain to arrive first.
            window.setTimeout( function() { 
               handle_sending_mK_data( mK);
            }, 100);
         }
      };
      
      function resetMouseOrFingerState( e) {
         // Process mouseup and touchend events.
         console.log(demoRunningOnHost + ',e.changedTouches=' + JSON.stringify(e.changedTouches) );
         
         var cl = referenceToClient();
         mK.MD = false; // Mouse Down (mouse button is up)
         mK.bu = 0;  // When mouse or touch is up, set button to default value of 0, the left button.
         
         if (twoThumbs.getEnabled() && e.changedTouches) {
            var releasePoint_2d_px = screenFromRaw_2d_px( clientCanvas_tt, new cP.Vec2D( e.changedTouches[0].clientX, e.changedTouches[0].clientY), {'inputDevice':'touchScreen', 'demoRunningOnHost':demoRunningOnHost});
            twoThumbs.processSingleTouchRelease(  releasePoint_2d_px);
         
         // Translate touch-screen events into keyboard data for pool shoots (see also handleMouseOrTouchMove)
         
         } else if ((demoRunningOnHost.slice(0,3) == "3.d") && e.changedTouches) {
            var touchID = e.changedTouches[0].identifier - ts.firstTouchPointID; // the relative touch id: 0,1,2,3...
            
            // If the first touch point is lifted: shoot the cue ball
            if (touchID == "0") {
               mK['z'] = 'U';
               mK['3'] = 'U';
               mK.MD = false; // shoot it
               
               // In the first two touch-release events, tell the host you're playing from a touchscreen. This is needed
               // for the ghost-ball pool (since the actual TwoThumbs interface is not used). However, this also fires for any
               // client using a touch screen. Note: this (touch-screen usage) will only show up in the leaderboard reporting for
               // ghost-ball pool if the network client takes a pool shot. The touchScreenUsage_sendCounter value is reset to zero 
               // on page load and also a normal re-connect with the client connect button.
               // (The "chat message" here, or ones similar, can be useful in debugging from the cell phone.)
               if ( gb.touchScreenUsage_sendCounter <= 1) {
                  // announce it just once
                  if ( gb.touchScreenUsage_sendCounter == 1) socket.emit('chat message', "touch screen in use");
                  // the control message will get sent twice, just to be sure
                  sendSocketControlMessage( {'from':cl.name, 'to':'host', 'data':{'touchScreenUsage':{'value':true}} } );
                  gb.touchScreenUsage_sendCounter += 1;
               }
            
            // If second touch point is lifted: reset the z key.
            } else if (touchID == "1") {
               mK['z'] = 'U';
               mK.MD = true; // don't shoot
               
            // If third touch point is lifted: do nothing.
            } else if (touchID == "2") {
               mK.MD = true; // don't shoot
               
            // If forth touch point is lifted: reset the 3 key (so ready to restart the game).
            } else if (touchID == "3") {
               mK['3'] = 'U';
               mK.MD = true; // don't shoot
            }
         }
         
         handle_sending_mK_data( mK);      
      }
      
      // Mouse-wheel events
      // (use document or videoMirror)
      videoMirrorDiv.addEventListener("wheel", function(e) {
         // Chrome doesn't seem to listen to these (in the normal way). Had to explicitly set passive:false (should be the default).
         // Also tried putting this listener on document, videoMirror, and videoMirrorDiv. But nothing works unless passive is false.
         e.preventDefault();
         // see style for videoMirror (touch-action: none;) in hostAndClient.css: stops scrolling and zooming behavior associated with mouse wheel.
         // Note the Chrome client can use a two-finger gesture on touch pad without getting scrolling/zooming behavior.
         
         if (e.deltaY < 0) {
            mK.mW = 'F';  // roll wheel forward
         } else {
            mK.mW = 'B';  // roll wheel back
         }
         handle_sending_mK_data( mK);
         mK.mW = 'N';

      }, {passive: false, capture: false});
      
      document.addEventListener("keydown", function( e) {
         // This allows the spacebar to be used for the puck shields.
         //console.log(e.keyCode + "(down)=" + String.fromCharCode(e.keyCode));
         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();
         }
         
         if (e.keyCode in keyMap_cso) {
            if (mK_cso[keyMap_cso[e.keyCode]] == 'U') {
               // Set the key to DOWN.
               mK_cso[keyMap_cso[e.keyCode]] = 'D';
            }
         }
         
         // Toggle the p2p connection (shift p)
         if ((mK_cso.key_p == 'D') && (mK_cso.key_shift == 'D')) {
            rtc_choke = !rtc_choke;
            refresh_P2P_indicator({'mode':'p2p', 'context':'chokeToggle'});
         
         // Esc out of full-screen mode (only mildly useful if the twothumbs checkbox is not hidden) 
         // If you're in fullscreen mode, this one won't
         // be the first to fire. The fullscreenchange handler fires first. Then, after
         // a second esc key press, this block will execute.
         } else if (keyMap_cso[e.keyCode] == 'key_esc') {
            // Reveal the video element (and hide the canvas).
            videoMirror.removeAttribute("hidden");
            clientCanvas_tt.setAttribute("hidden", null);
            
            chkTwoThumbs.checked = false;
            twoThumbs.setEnabled(false);
         }
         
         if (e.keyCode in keyMap) {
            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) {
         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).
      
      // Video stream checkbox.
      chkRequestStream = document.getElementById('chkRequestStream');
      chkRequestStream.checked = false; 
      chkRequestStream.addEventListener("click", function() {
         var cl = referenceToClient();
         // You checked it.
         if (chkRequestStream.checked) {
            // For now, leaving the full-screen button enabled at all times.
            //$('#FullScreen').prop('disabled', false);
            if ($('#roomName').val() == "") {
               displayMessage('');
               displayMessage('You must have a room name in the red box. Try again.');
               displayMessage('');
               chkRequestStream.checked = false;
               
            } else {
               if (chkTwoThumbs.checked) {
                  // Uncheck twoThumbs (but it's probably hidden unless I'm testing)
                  chkTwoThumbs.click();
               }
               // re-negotiate the connection.
               window.setTimeout( function() {
                  connect_and_listen('re-connect-with-stream');
               }, 100);
            }
         // You unchecked it.
         } else {
            // For now, leaving the full-screen button enabled at all times.
            //$('#FullScreen').prop('disabled', true);
            if (socket) {
               var control_message = {'from':cl.name, 'to':'host', 'data':{'videoStream':'off'} };
               socket.emit('control message', JSON.stringify( control_message));
               
               // Wait a bit for the above message to get to the host. Then clean out the
               // video element.
               window.setTimeout(function() {
                  if (videoMirror.srcObject) videoMirror.srcObject = null;
               }, 200);
               
            } else {
               displayMessage('');
               displayMessage("If you haven't already, please connect to the host.");
            }
         }
      }, {capture: false});
      
      // This control can be useful for testing but is normally hidden. Edit client.html
      // to un-hide it.
      chkTwoThumbs = document.getElementById('chkTwoThumbs');
      chkTwoThumbs.checked = false;
      chkTwoThumbs.addEventListener("click", function() {
         if (chkTwoThumbs.checked) {
            twoThumbs.changeDisplay('normal');
         } else {
            twoThumbs.changeDisplay('exit');
         }
      }, {capture: false});
      
      // Button (on client) for starting the TwoThumbs interface
      btnTwoThumbs = document.getElementById('twoThumbsButton');
      btnTwoThumbs.addEventListener("click", function() {
         twoThumbs.changeDisplay('fullScreen');
      }, {capture: false});
      
      // Button (on client) for viewing the stream full-screen
      btnFullScreen = document.getElementById('btnFullScreen_Client');
      btnFullScreen.addEventListener('click', function() {
         changeFullScreenMode( videoMirror, 'on');
      }, {capture: false});
      
      // Local cursor is handy if the engine is paused. Also give visual indicator of lag.
      chkLocalCursor = document.getElementById('chkLocalCursor');
      chkLocalCursor.checked = true;
      chkLocalCursor.addEventListener("click", function() {
         if (chkLocalCursor.checked) {
            videoMirror.style.cursor = 'default';
            clientCanvas_tt.style.cursor = 'default';
         } else {
            videoMirror.style.cursor = 'none';
            clientCanvas_tt.style.cursor = 'none';
         }
      }, {capture: false});
      
      // Option for connecting without a puck.
      chkPlayer = document.getElementById('chkPlayer');
      chkPlayer.checked = true;
      
      // For general handling of changes in fullscreen state.
      // Useful for handling the first press of the ESC key (exiting fullscreen mode)
      $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange msfullscreenchange', function(e) {
         // Check for fullscreen-state change.
         
         // Starting fullscreen
         if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
            console.log('fullscreen state: TRUE');
            gW.fullScreenState('on');
            videoMirror.style.borderWidth = '0px';  // 0px
            
         // Exiting fullscreen
         } else {
            console.log('fullscreen state: FALSE');
            gW.fullScreenState('off');
            clientCanvas_tt.width  = videoMirror.width;
            clientCanvas_tt.height = videoMirror.height;
            videoMirror.style.borderWidth = '5px';  // 5px
            // The following statement is needed for Firefox, video streaming,
            // and hiding the two-thumbs display (and revealing the video element).
            twoThumbs.changeDisplay('exit');
         }
      });
      
   }

   // Reveal public pointers to private objects ///////////////

   return {
      
      RTC: RTC,
      gb: gb,
      
      getClientDeviceType: function() { return clientDeviceType; },
      
      //nodeServerURL: nodeServerURL,
      forceClientDisconnect: forceClientDisconnect,
      resizeClients: resizeClients,
      sendSocketControlMessage: sendSocketControlMessage,
      init_chatFeatures: init_chatFeatures,
      init_nonHostClients: init_nonHostClients,
      connect_and_listen: connect_and_listen,
      refresh_P2P_indicator: refresh_P2P_indicator,
      setCanvasStream: setCanvasStream,
      changeFullScreenMode: changeFullScreenMode,
      chatToNonHostPlayers: chatToNonHostPlayers,
      displayMessage: displayMessage,
      checkForNickName: checkForNickName,
      clearInputDefault: clearInputDefault,
      restoreInputDefault: restoreInputDefault,
      clientColor: clientColor,
      initialize_mK: initialize_mK,
      handle_sending_mK_data: handle_sending_mK_data
      
   };
   
})();