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