messenger.handlebars 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. <!DOCTYPE html>
  2. <html lang="en" style=height:100%>
  3. <head>
  4. <title>{{{title}}} - Messenger</title>
  5. <meta http-equiv=X-UA-Compatible content="IE=edge">
  6. <meta content="text/html;charset=utf-8" http-equiv=Content-Type>
  7. <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
  8. <meta name=format-detection content="telephone=no">
  9. <meta name="robots" content="noindex,nofollow">
  10. <link type="text/css" href="styles/style.css" media="screen" rel="stylesheet" title="CSS" />
  11. <link type="text/css" href="styles/messenger.css" media="screen" rel="stylesheet" title="CSS" />
  12. {{{customCSSTags}}}
  13. <link rel="apple-touch-icon" href="/favicon-303x303.png" />
  14. <script type="text/javascript" src="scripts/common-0.0.1{{min}}.js"></script>
  15. <script type="text/javascript" src="scripts/filesaver.min.js"></script>
  16. {{{customJSTags}}}
  17. </head>
  18. <body style="font-family:Arial,Helvetica,sans-serif">
  19. <div id="xtop" style="position:absolute;left:0;right:0;top:0;height:38px;background-color:#036;color:#EEE;box-shadow:3px 3px 10px gray">
  20. <img style="float:left" height="38" src="messenger.png" />
  21. <div style="position:absolute;background-color:#036;right:0;height:38px">
  22. <div id="saveButton" class="icon15 topButton" style="margin-right:4px" title="Save conversation" onclick="saveChatSession()"></div>
  23. <div id="notifyButton" class="icon13 topButton" style="display:none" title="Enable browser notification" onclick="enableNotificationsButtonClick()"></div>
  24. <div id="fileButton" class="icon4 topButton" title="Share a file" style="display:none" onclick="fileButtonClick()"></div>
  25. <div id="camButton" class="icon2 topButton" title="Activate camera & microphone" style="display:none" onclick="camButtonClick()"></div>
  26. <div id="micButton" class="icon6 topButton" title="Activate microphone" style="display:none" onclick="micButtonClick()"></div>
  27. <div id="hangupButton" class="icon11 topRedButton" title="Hang up" style="display:none" onclick="hangUpButtonClick(1)"></div>
  28. <div id="recordIcon" class='deskareaicon' title="Server is recording this session" style="background-color:red;margin:6px;margin-top:7px;border-radius:12px;height:24px;width:24px;float:right;display:none"></div>
  29. </div>
  30. <div style="padding-top:9px;padding-left:6px;font-size:20px;display:inline-block"><b><span id="xtitle"></span></b></div>
  31. </div>
  32. <div id="xmiddle" style="position:absolute;left:0;right:0;top:38px;bottom:36px;font-size:18px">
  33. <div id="xmsgparent" style="position:absolute;left:0;right:0;bottom:0;max-height:100%;overflow-y:auto">
  34. <div id="xmsg" style="padding:5px"></div>
  35. <div id="typingIndicator" style="display:none;margin-left:5px;clear:both"><img src="images/3dots-24.gif" srcset="images/3dots-48.gif 2x" /></div>
  36. </div>
  37. </div>
  38. <div id="xbottom" style="position:absolute;left:0;right:0;bottom:0px;height:36px;background-color:#036;font-size:18px">
  39. <table style="width:100%">
  40. <tr>
  41. <td>
  42. <input id="xouttext" type="text" style="box-sizing:border-box;width:100%;font-size:18px" onfocus=onUserInputFocus(1) onblur=onUserInputFocus(0) onkeyup="updateLocalOutText()" />
  43. </td>
  44. <td style="width:1px">
  45. <input type="button" id="sendButton" value="Send" style="box-sizing: border-box;float:right;font-size:18px" onclick="xsend(event)" />
  46. </td>
  47. <td style="width:1px">
  48. <input type="button" id="clearButton" value="Clear" style="box-sizing: border-box;float:right;font-size:18px" onclick="displayClear()" />
  49. </td>
  50. </tr>
  51. </table>
  52. </div>
  53. <div id="remoteVideo" style="position:absolute;right:24px;top:45px;width:320px;height:calc(240px + 30px);background-color:gray;border-radius:12px 12px 12px 12px;box-shadow:3px 3px 10px gray;display:none">
  54. <div style="position:absolute;right:0;left:0;top:2.5px;text-align:center">Remote</div>
  55. <video id="remoteVideoCanvas" autoplay style="position:absolute;top:20px;left:0;width:100%;height:calc(100% - 30px);background-color:black" onclick="remotePlay(event)"></video>
  56. <div id="remoteClickToView" style="position:absolute;top:20px;left:0;width:100%;height:calc(100% - 30px);color:white;text-align:center;padding-top:20px;display:none" onclick="remotePlay(event)">Click here to view.</div>
  57. </div>
  58. <div id="localVideo" style="position:absolute;right:24px;top:320px;width:160px;height:calc(120px + 30px);background-color:gray;border-radius:12px 12px 12px 12px;box-shadow:3px 3px 10px gray;display:none">
  59. <div style="position:absolute;right:0;left:0;top:2.5px;text-align:center">Local</div>
  60. <video id="localVideoCanvas" autoplay muted style="position:absolute;top:20px;left:0;right:100%;height:calc(100% - 30px);background-color:black"></video>
  61. </div>
  62. <canvas width="256" height="256" id="remoteImage" style="position:absolute;right:24px;top:45px;width:200px;height:200px;background-color:gray;border-radius:12px 12px 12px 12px;box-shadow:3px 3px 10px gray;display:none" />
  63. <input id="uploadFileInput" type="file" multiple style="display:none">
  64. <script type="text/javascript" onunload="onUnLoad()">
  65. var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
  66. var userInputFocus = 0;
  67. var socket = null; // Websocket object
  68. var state = 0; // Connection state. 0 = Disconnected, 1 = Connecting, 2 = Connected.
  69. var args = XparseUriArgs();
  70. if (args.key && (isAlphaNumeric(args.key) == false)) { delete args.key; }
  71. if (args.locale && (isAlphaNumeric(args.locale) == false)) { delete args.locale; }
  72. var pushMessaging = (args.pmt == 1);
  73. // WebRTC sessions and data, audio and video channels
  74. var random = Math.random(); // Selected random, larger value initiates WebRTC.
  75. var webrtcSessions = { }; // WebRTC objects: 0 for data, 1 for outbound audio/video, 2 for inbound audio/video
  76. var webchannel = null; // WebRTC data channel
  77. var localStream = null;
  78. var remoteStream = null;
  79. var multiWebRtc = true; // if set to true, multiple WebRTC sessions will be setup. If false, everything uses one session.
  80. var userMediaSupport = 0;
  81. var notification = null;
  82. getUserMediaSupport(function (x) { userMediaSupport = x; })
  83. var meshMessengerTitle = '{{{meshMessengerTitle}}}';
  84. var meshMessengerImage = '{{{meshMessengerImage}}}';
  85. var remoteUserName = '{{{username}}}';
  86. var remoteUserId = '{{{userid}}}';
  87. var webrtcconfiguration = '{{{webrtcconfig}}}';
  88. if (webrtcconfiguration == '') { webrtcconfiguration = null; } else { try { webrtcconfiguration = JSON.parse(decodeURIComponent(webrtcconfiguration)); } catch (ex) { console.log('Invalid WebRTC config: "' + webrtcconfiguration + '".'); webrtcconfiguration = null; } }
  89. var chatTextSession = new Date().toString() + '\r\n';
  90. var localOutText = false;
  91. var remoteOutText = false;
  92. var remoteImage = false;
  93. var serverRecording = false;
  94. var notificationSupport = true;
  95. // File transfer state
  96. var fileUploads = [];
  97. var fileDownloads = {};
  98. var currentFileUpload = null;
  99. var currentFileDownload = null;
  100. setInterval(resizeVideos, 1000);
  101. function resizeVideos() {
  102. var rheight = 0;
  103. if (remoteStream != null) {
  104. rheight = ((320 * Q('remoteVideoCanvas').videoHeight) / Q('remoteVideoCanvas').videoWidth);
  105. QS('remoteVideo').height = 'calc(' + rheight + 'px + 30px)';
  106. } else {
  107. if (remoteImage == true) { rheight += 200; }
  108. }
  109. if (localStream != null) {
  110. QS('localVideo').height = 'calc(' + ((160 * Q('localVideoCanvas').videoHeight) / Q('localVideoCanvas').videoWidth) + 'px + 30px)';
  111. QS('localVideo').top = (rheight + 50 + ((remoteStream != null)?30:0)) + 'px';
  112. }
  113. }
  114. // Set the title
  115. var newTitle = '';
  116. if (args.title) { newTitle = decodeURIComponent(args.title); document.title = document.title + ' - ' + decodeURIComponent(args.title); }
  117. else if (meshMessengerTitle == '!') { newTitle = "MeshMessenger"; } else { newTitle = decodeURIComponent(meshMessengerTitle); }
  118. newTitle = newTitle.split('{0}').join(decodeURIComponent(remoteUserName)).split('{1}').join(decodeURIComponent(remoteUserId));
  119. QH('xtitle', EscapeHtml(newTitle).split(' ').join('&nbsp'));
  120. // Setup web notifications
  121. try { if (Notification) { QV('notifyButton', Notification.permission != 'granted'); } } catch (ex) { notificationSupport = false; }
  122. // Listen to drag & drop events
  123. document.addEventListener('dragover', haltEvent, false);
  124. document.addEventListener('dragleave', haltEvent, false);
  125. document.addEventListener('drop', fileDrop, false);
  126. document.onclick = function (e) {
  127. if (notificationSupport) {
  128. if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
  129. if (notification != null) { notification.close(); notification = null; }
  130. }
  131. }
  132. // Trap document key up events
  133. document.onkeyup = function ondockeypress(e) {
  134. if (notificationSupport) {
  135. if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
  136. if (notification != null) { notification.close(); notification = null; }
  137. }
  138. if (state == 2) {
  139. if ((e.keyCode == 8) && (userInputFocus == 0)) {
  140. // Backspace
  141. var outtext = Q('xouttext').value;
  142. if (outtext.length > 0) { Q('xouttext').value = outtext.substring(0, outtext.length - 1); }
  143. updateLocalOutText();
  144. }
  145. }
  146. if (userInputFocus == 0) { haltEvent(e); return false; }
  147. }
  148. // Trap document key presses
  149. document.onkeypress = function ondockeypress(e) {
  150. if (notificationSupport) {
  151. if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
  152. if (notification != null) { notification.close(); notification = null; }
  153. }
  154. if ((state == 2) || pushMessaging) {
  155. if (e.keyCode == 13) {
  156. // Return
  157. xsend(e);
  158. } else {
  159. // Any other key
  160. if ((userInputFocus == 0) && (e.key.length == 1)) { Q('xouttext').value = Q('xouttext').value + e.key; updateLocalOutText(); }
  161. }
  162. }
  163. if (userInputFocus == 0) { haltEvent(e); return false; }
  164. }
  165. function onUserInputFocus(x) { userInputFocus = x; }
  166. function displayClear() { chatTextSession = new Date().toString() + '\r\n'; QH('xmsg', ''); cancelAllFileTransfers(); fileUploads = [], fileDownloads = {}; }
  167. // Polyfill FileReader if needed
  168. if (!FileReader.prototype.readAsBinaryString) {
  169. FileReader.prototype.readAsBinaryString = function (fileData) {
  170. var binary = '', self = this, reader = new FileReader();
  171. reader.onload = function (e) {
  172. var bytes = new Uint8Array(reader.result);
  173. for (var i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); }
  174. self.onload({ target: { result: binary } });
  175. }
  176. reader.readAsArrayBuffer(fileData);
  177. }
  178. }
  179. // Detect if microphone & camera are present
  180. // 0 = nomedia, 1 = miconly, 2 = mic&cam
  181. function getUserMediaSupport(func) {
  182. try {
  183. navigator.mediaDevices.enumerateDevices().then(function (devices) {
  184. try {
  185. var mic = 0, cam = 0;
  186. devices.forEach(function (device) {
  187. if (device.kind === 'audioinput') { mic = 1; }
  188. if (device.kind === 'videoinput') { cam = 1; }
  189. });
  190. if (mic == 0) { func(0); }
  191. func(mic + cam);
  192. } catch (ex) { }
  193. })
  194. } catch (ex) { }
  195. }
  196. // Display a control message
  197. function displayControl(msg) {
  198. chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Control" + '> ' + msg + '\r\n');
  199. QA('xmsg', '<div style="clear:both"><div style="color:gray;float:left;margin-bottom:2px">' + EscapeHtml(msg) + '</div><div></div></div>');
  200. Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;//Q('xmsg').scrollHeight;
  201. }
  202. function displayLocalVideo(active) { QV('localVideo', active); adjustVideoWindows(); }
  203. function displayRemoteVideo(active) { QV('remoteVideo', active); adjustVideoWindows(); }
  204. function adjustVideoWindows() {
  205. var lv = (QS('localVideo')['display'] != 'none');
  206. var rv = (QS('remoteVideo')['display'] != 'none');
  207. QV('remoteImage', (remoteImage == true) && (rv == false))
  208. if (rv) { QS('localVideo')['top'] = '320px'; }
  209. else if (remoteImage) { QS('localVideo')['top'] = '320px'; }
  210. else { QS('localVideo')['top'] = '45px'; }
  211. resizeVideos();
  212. }
  213. // Display a message from the remote user
  214. function displayRemote(msg) {
  215. chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Remote" + '> ' + msg + '\r\n');
  216. QA('xmsg', '<div style="clear:both"><div class="remoteBubble">' + EscapeHtml(msg) + '</div><div></div></div>');
  217. Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
  218. // If web notifications are granted, use it.
  219. if (notificationSupport) {
  220. if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
  221. if (Notification && (document.visibilityState === 'hidden') && (Notification.permission == 'granted')) {
  222. if (notification != null) { notification.close(); notification = null; }
  223. notification = new Notification(Q('xtitle').innerHTML.split('&nbsp;').join(' '), { body: msg });
  224. notification.onclick = function(event) {
  225. event.preventDefault();
  226. if (document.visibilityState === 'hidden') window.focus();
  227. notification.close();
  228. };
  229. }
  230. }
  231. }
  232. // Display and send a message from the local user
  233. function xsend(event) {
  234. if (notificationSupport) {
  235. if (notification != null) { notification.close(); notification = null; }
  236. if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
  237. }
  238. var outtext = Q('xouttext').value;
  239. if (outtext.length > 0) {
  240. chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Local" + '> ' + outtext + '\r\n');
  241. QA('xmsg', '<div style="clear:both"><div class="localBubble">' + EscapeHtml(outtext) + '</div><div></div></div>');
  242. Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
  243. if ((state == 2) || pushMessaging) { send({ action: 'chat', msg: outtext }); }
  244. Q('xouttext').value = '';
  245. Q('xouttext').focus();
  246. localOutText = false;
  247. }
  248. }
  249. function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
  250. // Update user controls
  251. function updateControls() {
  252. QE('sendButton', (state == 2) || pushMessaging);
  253. QE('clearButton', (state == 2) || pushMessaging);
  254. QE('xouttext', (state == 2) || pushMessaging);
  255. QV('fileButton', state == 2);
  256. QV('camButton', webchannel && webchannel.ok && !localStream && (userMediaSupport == 2));
  257. QV('micButton', webchannel && webchannel.ok && !localStream && (userMediaSupport > 0));
  258. QV('hangupButton', webchannel && webchannel.ok && localStream);
  259. updateLocalOutText();
  260. }
  261. // This is the WebRTC setup
  262. function startWebRTC(id, startDataChannel) {
  263. if ((webrtcSessions[0] != null) && (multiWebRtc == false)) { return webrtcSessions[0]; };
  264. // Setup the WebRTC object
  265. var webrtc = null;
  266. if (typeof RTCPeerConnection !== 'undefined') { webrtc = new RTCPeerConnection(webrtcconfiguration); }
  267. else if (typeof webkitRTCPeerConnection !== 'undefined') { webrtc = new webkitRTCPeerConnection(webrtcconfiguration); }
  268. if (webrtc == null) return null; // No WebRTC support.
  269. webrtc.id = id;
  270. webrtc.onicecandidate = function (e) { try { if (e.candidate != null) { sendws({ action: 'webRtcIce', ice: e.candidate, id: this.id }); } } catch (ex) { } }
  271. webrtc.oniceconnectionstatechange = function () { if (webrtc && webrtc.iceConnectionState == 'failed') { webrtc.close(); if (webrtcSessions[webrtc.id]) { delete webrtcSessions[webrtc.id]; } } }
  272. webrtc.ondatachannel = function (ev) {
  273. //console.log('ondatachannel');
  274. webchannel = ev.channel;
  275. webchannel.onmessage = function (event) { processMessage(event.data, 2); };
  276. webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
  277. webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
  278. }
  279. webrtc.onnegotiationneeded = function (event) {
  280. if (webrtc.holdTimer != null) return;
  281. webrtc.holdTimer = setTimeout(function () { // This time is needed to keep Chrome from being to excited. Wait until we add all tracks before kicking this off.
  282. //console.log('onnegotiationneeded', id);
  283. webrtc.holdTimer = null;
  284. webrtc.createOffer(function (offer) { /*console.log('offer', offer.sdp.length);*/ webrtc.setLocalDescription(offer, function () { sendws({ action: 'webRtcSdp', sdp: offer, id: id }); }, function () { hangUpButtonClick(id); }); }, function () { hangUpButtonClick(id); });
  285. }, 20);
  286. }
  287. webrtc.ontrack = function (event) {
  288. //console.log('ontrack', id);
  289. QV('remoteClickToView', false);
  290. var video = Q('remoteVideoCanvas');
  291. video.srcObject = remoteStream = event.streams[0];
  292. video.onloadedmetadata = function (e) {
  293. var promise = video.play();
  294. if (promise !== undefined) {
  295. promise.then(function() {
  296. // Video start ok
  297. }).catch(function(err) {
  298. // Video start error, display a play button
  299. QV('remoteClickToView', true);
  300. })
  301. }
  302. };
  303. displayRemoteVideo(true);
  304. }
  305. //webrtc.onremovetrack = function (event) { console.log('onremovetrack'); }
  306. //webrtc.onicegatheringstatechange = function (event) { console.log('onicegatheringstatechange', event); }
  307. //webrtc.onsignalingstatechange = function (event) { console.log('onsignalingstatechange', event); }
  308. // Initiate the WebRTC offer or handle the offer from the peer.
  309. if (startDataChannel == true) {
  310. webchannel = webrtc.createDataChannel('DataChannel', {}); // { ordered: false, maxRetransmits: 2 }
  311. webchannel.onmessage = function (event) { processMessage(event.data, 2); };
  312. webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
  313. webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
  314. }
  315. webrtcSessions[id] = webrtc;
  316. return webrtc;
  317. }
  318. function remotePlay() { QV('remoteClickToView', false); Q('remoteVideoCanvas').play(); }
  319. function webRtcHandleOffer(id, description) {
  320. //console.log('webRtcHandleOffer', description.sdp.length);
  321. var webrtc = webrtcSessions[id];
  322. if (webrtc) {
  323. webrtc.setRemoteDescription(new RTCSessionDescription(description), function () {
  324. if (description.type == 'offer') {
  325. webrtc.createAnswer(function (answer) {
  326. webrtc.setLocalDescription(answer, function (a, b) {
  327. try { sendws({ action: 'webRtcSdp', sdp: answer, id: id }); } catch (ex) { }
  328. }, function () { hangUpButtonClick(id); });
  329. }, function () { hangUpButtonClick(id); });
  330. }
  331. }, function () { hangUpButtonClick(id); });
  332. }
  333. }
  334. // Indicate to peer that data traffic will no longer be sent over websocket and start holding traffic.
  335. function performWebRtcSwitch() {
  336. if (!serverRecording && webchannel && webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; }
  337. }
  338. // Disconnect everything
  339. function disconnect() {
  340. serverRecording = false;
  341. QV('recordIcon', false);
  342. if (state > 0) { displayControl("Connection closed."); }
  343. if (state > 1) { setTimeout(start, 500); }
  344. cancelAllFileTransfers();
  345. hangUpButtonClick(0, true); // Data channel
  346. hangUpButtonClick(1, true); // Local audio/video
  347. hangUpButtonClick(2, true); // Remote audio/video
  348. if (socket != null) { socket.close(); socket = null; }
  349. updateControls();
  350. state = 0;
  351. remoteImage = false;
  352. QV('remoteImage', false);
  353. updateLocalOutText();
  354. updateRemoteOutText();
  355. }
  356. // Send data over the current transport (WebRTC first)
  357. function send(data) {
  358. if ((state != 2) && (pushMessaging == false)) return; // If not in connected state, ignore this.
  359. if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
  360. if (!serverRecording && webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it.
  361. else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that.
  362. }
  363. // Send data over the websocket transport (WebSocket only)
  364. function sendws(data) {
  365. if (state != 2) return;
  366. //console.log('SEND', data);
  367. if (typeof data == 'object') { data = JSON.stringify(data); }
  368. if (socket != null) { socket.send(data); }
  369. }
  370. // WebRTC id switcher (0 -> 0, 1 -> 2, 2 -> 1)
  371. function webRtcIdSwitch(id) { if (id == 0) { return 0; } return 3 - id; }
  372. function drawRemoteImage(imageBase64) {
  373. var canvas = Q('remoteImage'), context = canvas.getContext('2d'), img = new Image();
  374. img.onload = function () { context.drawImage(this, 0, 0, canvas.width, canvas.height); remoteImage = true; adjustVideoWindows(); }
  375. img.src = imageBase64;
  376. }
  377. // Process incoming messages
  378. function processMessage(data, transport) {
  379. if (typeof data == 'string') {
  380. try { data = JSON.parse(data); } catch (ex) { console.log('Unable to parse', data); return; }
  381. //console.log('RECV', data);
  382. // Handle control command
  383. if (data.ctrlChannel == '102938') {
  384. switch (data.type) {
  385. case 'image': { drawRemoteImage(data.image); break; }
  386. case 'ping': { send({ ctrlChannel: '102938', type: 'pong' }); break; }
  387. case 'pong': { break; }
  388. }
  389. return;
  390. }
  391. // Handle command
  392. switch (data.action) {
  393. case 'chat': { displayRemote(data.msg); updateRemoteOutText(false); break; } // Incoming chat message.
  394. case 'outtext': { updateRemoteOutText(data.value); break; }
  395. case 'ctrl': {
  396. if (data.value == 1) { displayControl("Sent as push notification."); }
  397. else if (data.value == 2) { displayControl("Push notification failed."); }
  398. if (data.msg != null) { displayControl(msg); }
  399. break;
  400. }
  401. case 'random': { if (random > data.random) { startWebRTC(0, true); } break; } // If we have a larger random value, we start WebRTC.
  402. case 'webRtcSdp': { if (!webrtcSessions[webRtcIdSwitch(data.id)]) { startWebRTC(webRtcIdSwitch(data.id), false); } webRtcHandleOffer(webRtcIdSwitch(data.id), data.sdp); break; } // Remote WebRTC offer or answer.
  403. case 'webRtcIce': { var webrtc = webrtcSessions[webRtcIdSwitch(data.id)]; if (webrtc) { try { webrtc.addIceCandidate(new RTCIceCandidate(data.ice)); } catch (ex) { } } break; } // Remote ICE candidate
  404. case 'videoStop': { hangUpButtonClick(webRtcIdSwitch(data.id), true); break; }
  405. case 'rtcSwitch': { // WebRTC switch over commands.
  406. switch (data.v) {
  407. case 0: { performWebRtcSwitch(); break; } // Other side is ready for switch over to WebRTC
  408. case 1: { sendws({ action: 'rtcSwitch', v: 2 }); break; } // Other side no longer sending data on websocket, confirm we got the end marker
  409. case 2: { for (var i in webchannel.xoutBuffer) { webchannel.send(webchannel.xoutBuffer[i]); } delete webchannel.xoutBuffer; break; } // Send any pending data over WebRTC and start using WebRTC with all traffic
  410. default: { console.log('Unknown rtcSwitch value: ' + data.action); break; } //
  411. }
  412. break;
  413. }
  414. case 'file': { startFileDownload(data); break; }
  415. case 'fileUploadCancel': { cancelFileTransfer(data.id); break; }
  416. case 'fileUploadStart': {
  417. if (fileDownloads[data.id]) {
  418. currentFileDownload = fileDownloads[data.id];
  419. currentFileDownload.data = '';
  420. changeFileInfo(data.id, 2, 0);
  421. continueFileDownload(data);
  422. send({ action: 'fileUploadAck', id: data.id });
  423. } break;
  424. }
  425. case 'fileUploadEnd': {
  426. if (currentFileDownload && (currentFileDownload.id == data.id)) {
  427. changeFileInfo(data.id, 3, 200);
  428. currentFileDownload.done = 1;
  429. currentFileDownload = null;
  430. send({ action: 'fileUploadAck', id: data.id });
  431. }
  432. currentFileDownload = null;
  433. break;
  434. }
  435. case 'fileUploadAck': {
  436. continueFileUpload();
  437. break;
  438. }
  439. case 'fileData': {
  440. if (currentFileDownload && (currentFileDownload.id == data.id)) {
  441. currentFileDownload.data += data.data;
  442. changeFileInfo(data.id, 2, (currentFileDownload.data.length * 200 / currentFileDownload.size));
  443. send({ action: 'fileUploadAck', id: data.id });
  444. }
  445. break;
  446. }
  447. default: { console.log('Unhandled object data', data); break; }
  448. }
  449. } else {
  450. console.log('Unhandled data', typeof data, data);
  451. }
  452. }
  453. // File sharing button
  454. function fileButtonClick() {
  455. var chooser = Q('uploadFileInput');
  456. if (chooser.getAttribute('eventset') != 1) {
  457. chooser.setAttribute('eventset', '1');
  458. chooser.addEventListener('change', fileSelect, false);
  459. }
  460. chooser.value = null;
  461. chooser.click();
  462. }
  463. // User selected one or more files to upload to remote user.
  464. function fileSelect() {
  465. if (state != 2) return;
  466. var x = Q('uploadFileInput');
  467. if (x.files.length > 10) {
  468. displayControl("Limit of 10 file uploads at the same time.");
  469. } else {
  470. for (var i = 0; i < x.files.length; i++) {
  471. if (x.files[i].size > 0) {
  472. var reader = new FileReader();
  473. reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
  474. reader.xfile = x.files[i];
  475. reader.readAsBinaryString(x.files[i]);
  476. }
  477. }
  478. }
  479. }
  480. // User drag & droped one or more files to upload to remote user.
  481. function fileDrop(e) {
  482. haltEvent(e);
  483. if ((state != 2) || (e.dataTransfer == null)) return;
  484. if (e.dataTransfer.files.length > 10) {
  485. displayControl("Limit of 10 file uploads at the same time.");
  486. } else {
  487. for (var i = 0; i < e.dataTransfer.files.length; i++) {
  488. if (e.dataTransfer.files[i].size > 0) {
  489. var reader = new FileReader();
  490. reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
  491. reader.xfile = e.dataTransfer.files[i];
  492. reader.readAsBinaryString(e.dataTransfer.files[i]);
  493. }
  494. }
  495. }
  496. }
  497. function startFileUpload(file) {
  498. if (state != 2) return;
  499. file.id = Math.random();
  500. fileUploads.push(file);
  501. chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Upload" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
  502. QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="localBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="cancelFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
  503. Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
  504. send({ action: 'file', size: file.size, id: file.id, type: file.type, name: file.name });
  505. if (currentFileUpload == null) continueFileUpload();
  506. }
  507. function startFileDownload(file) {
  508. if (state != 2) return;
  509. fileDownloads[file.id] = file;
  510. chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Download" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
  511. QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="remoteBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="saveFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
  512. Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
  513. }
  514. // Change the file icon and progress
  515. function changeFileInfo(id, icon, progress, progressColor) {
  516. if (icon) {
  517. Q('FILEUP-ICON-' + id).classList.remove('fileicon');
  518. Q('FILEUP-ICON-' + id).classList.remove('fileiconx');
  519. Q('FILEUP-ICON-' + id).classList.remove('fileicontransfer');
  520. Q('FILEUP-ICON-' + id).classList.remove('fileicondone');
  521. Q('FILEUP-ICON-' + id).classList.add(['fileicon', 'fileiconx', 'fileicontransfer', 'fileicondone'][icon]);
  522. }
  523. if (progress) { QS('FILEUP-PROGRESS-' + id)['width'] = progress + 'px'; }
  524. if (progressColor) { QS('FILEUP-PROGRESS-' + id)['background-color'] = progressColor; }
  525. }
  526. // Convert a string into a blob
  527. function data2blob(data) {
  528. var bytes = new Array(data.length);
  529. for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
  530. return new Blob([new Uint8Array(bytes)]);
  531. };
  532. function saveFileTransfer(id) {
  533. var f = fileDownloads[id];
  534. if (f && f.done == 1) { saveAs(data2blob(f.data), f.name); }
  535. }
  536. function cancelFileTransfer(id) {
  537. if ((currentFileUpload != null) && (currentFileUpload.id == id)) { currentFileUpload = null; }
  538. if ((currentFileDownload != null) && (currentFileDownload.id == id)) { currentFileDownload = null; }
  539. var found = false;
  540. if (fileDownloads[id] && (fileDownloads[id].done != 1)) {
  541. delete fileDownloads[id];
  542. found = true;
  543. } else {
  544. for (var i in fileUploads) {
  545. if (fileUploads[i].id == id) {
  546. send({ action: 'fileUploadCancel', id: id });
  547. fileUploads.splice(i, 1);
  548. found = true;
  549. break;
  550. }
  551. }
  552. }
  553. if (found) { changeFileInfo(id, 1, 200, 'gray'); } // Only cancel a file if it was in the file queue.
  554. }
  555. function cancelAllFileTransfers() {
  556. for (var i in fileDownloads) { cancelFileTransfer(fileDownloads[i].id); }
  557. for (var i in fileUploads) { cancelFileTransfer(fileUploads[i].id); }
  558. }
  559. function continueFileUpload() {
  560. if (currentFileUpload == null) {
  561. // Select the next file to upload
  562. if (fileUploads.length == 0) { return; } // Nothing to do
  563. currentFileUpload = fileUploads[0];
  564. currentFileUpload.ptr = 0;
  565. // Indicate that we are sending this file
  566. send({ action: 'fileUploadStart', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
  567. } else {
  568. if (currentFileUpload.size <= currentFileUpload.ptr) {
  569. // If we are done, send the end marker
  570. send({ action: 'fileUploadEnd', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
  571. changeFileInfo(currentFileUpload.id, 3, 200);
  572. fileUploads.splice(0, 1);
  573. currentFileUpload = null;
  574. continueFileUpload(); // Send the next file
  575. } else {
  576. // Send the next block
  577. var nextBlockLen = Math.min(4000, currentFileUpload.data.length - currentFileUpload.ptr);
  578. var data = currentFileUpload.data.substring(currentFileUpload.ptr, currentFileUpload.ptr + nextBlockLen);
  579. send({ action: 'fileData', id: currentFileUpload.id, data: data });
  580. currentFileUpload.ptr += nextBlockLen;
  581. changeFileInfo(currentFileUpload.id, 0, (currentFileUpload.ptr * 200 / currentFileUpload.size));
  582. }
  583. }
  584. }
  585. function continueFileDownload(msg) {
  586. send({ action: 'fileUploadAck', id: msg.id });
  587. }
  588. // Toggle notification
  589. function enableNotificationsButtonClick() {
  590. if (notificationSupport) {
  591. if (Notification) { Notification.requestPermission().then(function (permission) { QV('notifyButton', permission != 'granted'); }); }
  592. }
  593. return false;
  594. }
  595. // Camera button
  596. function camButtonClick() {
  597. if (localStream == null) { startLocalStream({ video: true, audio: true }); }
  598. }
  599. // Microphone
  600. function micButtonClick() {
  601. if (localStream == null) { startLocalStream({ video: false, audio: true }); }
  602. }
  603. function hangUpButtonClick(id, fromRemote) {
  604. //console.log('hangUpButtonClick', id);
  605. var localVideo = Q('localVideoCanvas');
  606. var remoteVideo = Q('remoteVideoCanvas');
  607. var webrtc = webrtcSessions[(multiWebRtc == true)? id : 0];
  608. if ((id == 0) && (webchannel != null)) { try { webchannel.close(); } catch (e) { } webchannel = null; }
  609. if (webrtc) {
  610. if ((multiWebRtc == true) || (id == 0)) {
  611. webrtc.ontrack = null;
  612. webrtc.onremovetrack = null;
  613. webrtc.onremovestream = null;
  614. webrtc.onnicecandidate = null;
  615. webrtc.oniceconnectionstatechange = null;
  616. webrtc.onsignalingstatechange = null;
  617. webrtc.onicegatheringstatechange = null;
  618. webrtc.onnotificationneeded = null;
  619. }
  620. if ((id == 1) && localStream) { var tracks = localStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } localStream = null; }
  621. if ((id == 2) && remoteStream) { var tracks = remoteStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } remoteStream = null; }
  622. if ((multiWebRtc == true) || (id == 0)) {
  623. webrtc.close();
  624. delete webrtcSessions[id];
  625. }
  626. }
  627. if (id == 1) {
  628. localVideo.removeAttribute('src');
  629. localVideo.removeAttribute('srcObject');
  630. if (localStream != null) { localStream = null; }
  631. displayLocalVideo(false);
  632. } else if (id == 2) {
  633. remoteVideo.removeAttribute('src');
  634. remoteVideo.removeAttribute('srcObject');
  635. displayRemoteVideo(false);
  636. }
  637. if (fromRemote != true) { send({ action: 'videoStop', id: id }); }
  638. updateControls();
  639. }
  640. // Setup local audio/video
  641. function startLocalStream(constraints) {
  642. var channel = (multiWebRtc == true) ? 1 : 0;
  643. if (localStream != null) return;
  644. if ((multiWebRtc == true) && (webrtcSessions[1] != null)) return;
  645. if (navigator.mediaDevices.getUserMedia) {
  646. localStream = 1;
  647. updateControls();
  648. navigator.mediaDevices.getUserMedia(constraints)
  649. .then(function (stream) {
  650. localStream = stream;
  651. var tracks = localStream.getTracks();
  652. var webrtc = startWebRTC(channel);
  653. if (constraints.video == true) {
  654. var video = Q('localVideoCanvas');
  655. video.srcObject = stream;
  656. video.onloadedmetadata = function (e) { video.play(); };
  657. displayLocalVideo(true);
  658. }
  659. for (var i in tracks) { webrtc.addTrack(tracks[i], localStream); }
  660. }, function (err) {
  661. displayControl(err.message + '.');
  662. hangUpButtonClick(1);
  663. });
  664. }
  665. }
  666. // This is the main start
  667. function start() {
  668. // Get started
  669. updateControls();
  670. if ((typeof args.id == 'string') && (args.id.length > 0)) {
  671. var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/meshrelay.ashx?id=' + args.id;
  672. if ((args.auth != null) && (args.auth != '')) { url += '&auth=' + args.auth; }
  673. socket = new WebSocket(url);
  674. socket.onopen = function () { state = 1; displayControl("Waiting for other user..."); }
  675. socket.onerror = function (e) { /*console.error(e);*/ }
  676. socket.onclose = function () { disconnect(); }
  677. socket.onmessage = function (msg) {
  678. if ((state < 2) && (typeof msg.data == 'string') && ((msg.data == 'c') || (msg.data == 'cr'))) {
  679. serverRecording = (msg.data == 'cr');
  680. QV('recordIcon', serverRecording);
  681. hangUpButtonClick(0, true);
  682. hangUpButtonClick(1, true);
  683. hangUpButtonClick(2, true);
  684. displayControl("Connected.");
  685. state = 2;
  686. updateControls();
  687. sendws({ action: 'random', random: random }); // Send a random number. Higher number starts the WebRTC session.
  688. updateLocalOutText();
  689. updateRemoteOutText();
  690. return;
  691. }
  692. if (msg.data[0] == '{') { processMessage(msg.data, 1); }
  693. }
  694. } else {
  695. displayControl("Error: No connection key specified.");
  696. }
  697. }
  698. function saveChatSession() {
  699. saveAs(data2blob(chatTextSession), "ChatSession");
  700. }
  701. start();
  702. function onUnLoad() {
  703. for (var i = 0; i < 3; i++) { if (webrtcSessions[i]) { try { webrtcSessions[i].close(); delete webrtcSessions[i]; } catch (ex) { } } }
  704. if (webchannel != null) { try { webchannel.close(); } catch (ex) { } webchannel = null; }
  705. if (socket != null) { try { socket.close(); } catch (ex) { } socket = null; }
  706. }
  707. function isSafeString3(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf(':') == -1)) };
  708. // Parse URL arguments, only keep safe values
  709. function XparseUriArgs() {
  710. var href = window.document.location.href;
  711. if (href.endsWith('#')) { href = href.substring(0, href.length - 1); }
  712. var name, r = {}, parsedUri = href.split(/[\?&|]/);
  713. parsedUri.splice(0, 1);
  714. for (var j in parsedUri) {
  715. var arg = parsedUri[j], i = arg.indexOf('=');
  716. name = arg.substring(0, i);
  717. r[name] = arg.substring(i + 1);
  718. if (!isSafeString3(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } }
  719. }
  720. return r;
  721. }
  722. function updateLocalOutText() {
  723. var l = Q('xouttext').value;
  724. if (((state != 2) || (l == '')) && (localOutText == true)) {
  725. localOutText = false;
  726. send({ action: 'outtext', value: false });
  727. } else if ((state == 2) && ((l != '') && (localOutText == false))) {
  728. localOutText = true;
  729. send({ action: 'outtext', value: true });
  730. }
  731. }
  732. function updateRemoteOutText(newState) {
  733. //console.log('updateRemoteOutText', newState);
  734. if (state != 2) { newState = false; }
  735. if (remoteOutText != newState) { remoteOutText = newState; QV('typingIndicator', remoteOutText); }
  736. }
  737. </script>
  738. </body>
  739. </html>