2018-11-29 20:59:29 -05:00
<!DOCTYPE html>
< html style = height:100% >
< head >
2018-12-02 02:41:57 -05:00
< title > MeshMessenger< / title >
2018-11-29 20:59:29 -05:00
< meta http-equiv = X-UA-Compatible content = "IE=edge" >
< meta content = "text/html;charset=utf-8" http-equiv = Content-Type >
< meta name = format-detection content = "telephone=no" >
< link type = "text/css" href = "styles/style.css" media = "screen" rel = "stylesheet" title = "CSS" / >
2018-12-16 16:44:49 -05:00
< link type = "text/css" href = "styles/messenger.css" media = "screen" rel = "stylesheet" title = "CSS" / >
2018-11-29 20:59:29 -05:00
< script type = "text/javascript" src = "scripts/common-0.0.1.js" > < / script >
< / head >
< body style = "font-family:Arial,Helvetica,sans-serif" >
2018-12-17 03:32:08 -05:00
< div id = "xtop" style = "position:absolute;left:0;right:0;top:0;height:38px;background-color:#036;color:#c8c8c8;box-shadow:3px 3px 10px gray" >
2018-12-16 16:44:49 -05:00
< div id = "fileButton" class = "icon4 topButton" title = "Share a file" style = "display:none" onclick = "fileButtonClick()" > < / div >
< div id = "camButton" class = "icon2 topButton" title = "Activate camera & microphone" style = "display:none" onclick = "camButtonClick()" > < / div >
< div id = "micButton" class = "icon6 topButton" title = "Activate microphone" style = "display:none" onclick = "micButtonClick()" > < / div >
2018-12-17 03:32:08 -05:00
< div id = "hangupButton" class = "icon11 topButton" title = "Hang up" style = "display:none" onclick = "hangUpButtonClick(1)" > < / div >
2018-12-16 16:44:49 -05:00
< div style = "display:inline-block;width:2px" > < / div >
2018-12-17 03:32:08 -05:00
< div style = "padding-top:9px;padding-left:6px;font-size:20px;display:inline-block" > < b > MeshMessenger< span id = "xtitle" > < / span > < / b > < / div >
2018-12-16 16:44:49 -05:00
< / div >
< div id = "xmiddle" style = "position:absolute;left:0;right:0;top:38px;bottom:30px" >
< div style = "position:absolute;left:0;right:0;top:0;bottom:0;overflow-y:scroll" >
< div id = "xmsg" style = "position:absolute;left:0;right:0;bottom:0;padding:5px" > < / div >
< / div >
2018-11-29 20:59:29 -05:00
< / div >
< div id = "xbottom" style = "position:absolute;left:0;right:0;bottom:0px;height:30px;background-color:#036" >
< div style = "position:absolute;left:5px;right:215px;bottom:4px;top:4px;background-color:aliceblue" > < input id = "xouttext" type = "text" style = "width:calc(100% - 5px)" onfocus = onUserInputFocus(1) onblur = onUserInputFocus(0) / > < / div >
< input type = "button" id = "sendButton" value = "Send" style = "position:absolute;right:110px;width:100px;top:4px;" onclick = "xsend(event)" / >
2018-12-16 03:17:26 -05:00
< input type = "button" id = "clearButton" value = "Clear" style = "position:absolute;right:5px;width:100px;top:4px;" onclick = "displayClear()" / >
2018-11-29 20:59:29 -05:00
< / div >
2018-12-17 03:32:08 -05:00
< 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" >
< div style = "position:absolute;right:0;left:0;top:2.5px;text-align:center" > Remote< / div >
< video id = "remoteVideoCanvas" autoplay style = "position:absolute;top:20px;left:0;width:100%;height:calc(100% - 30px);background-color:black" > < / video >
2018-12-16 16:44:49 -05:00
< / div >
2018-12-17 03:32:08 -05:00
< 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" >
< div style = "position:absolute;right:0;left:0;top:2.5px;text-align:center" > Local< / div >
< video id = "localVideoCanvas" autoplay muted style = "position:absolute;top:20px;left:0;width:100%;height:calc(100% - 30px);background-color:black" > < / video >
2018-12-16 16:44:49 -05:00
< / div >
2018-11-29 20:59:29 -05:00
< script type = "text/javascript" >
var userInputFocus = 0;
var args = parseUriArgs();
2018-12-17 03:32:08 -05:00
var socket = null; // Websocket object
var state = 0; // Connection state. 0 = Disconnected, 1 = Connecting, 2 = Connected.
var random = Math.random(); // Selected random, larger value initiates WebRTC.
var webrtcSessions = { }; // WebRTC objects: 0 for data, 1 for outbound audio/video, 2 for inbound audio/video
var webchannel = null; // WebRTC data channel
var webrtcconfiguration = null; //{ "iceServers": [ { 'urls': 'stun:stun.services.mozilla.com' }, { 'urls': 'stun:stun.l.google.com:19302' } ] };
2018-12-16 16:44:49 -05:00
var localStream = null;
2018-12-17 03:32:08 -05:00
var remoteStream = null;
var senders = null;
2018-11-29 20:59:29 -05:00
// Set the title
2018-12-02 02:41:57 -05:00
if (args.title) { QH('xtitle', ' - ' + args.title); document.title = document.title + ' - ' + args.title; }
2018-11-29 20:59:29 -05:00
2018-12-17 03:32:08 -05:00
document.onkeyup = function ondockeypress(e) {
if (state == 2) {
2018-11-29 20:59:29 -05:00
if ((e.keyCode == 8) & & (userInputFocus == 0)) {
// Backspace
var outtext = Q('xouttext').value;
if (outtext.length > 0) { Q('xouttext').value = outtext.substring(0, outtext.length - 1); }
2018-12-17 03:32:08 -05:00
}
}
if (userInputFocus == 0) { haltEvent(e); return false; }
}
// Trap document key presses
document.onkeypress = function ondockeypress(e) {
if (state == 2) {
if (e.keyCode == 13) {
2018-11-29 20:59:29 -05:00
// Return
xsend(e);
} else {
// Any other key
if ((userInputFocus == 0) & & (e.key.length == 1)) { Q('xouttext').value = Q('xouttext').value + e.key; }
}
}
if (userInputFocus == 0) { haltEvent(e); return false; }
}
function onUserInputFocus(x) { userInputFocus = x; }
2018-12-16 03:17:26 -05:00
function displayClear() { QH('xmsg', ''); }
// Display a control message
function displayControl(msg) {
2018-11-29 20:59:29 -05:00
QA('xmsg', '< div style = "clear:both" > < div style = "color:gray;float:left;margin-bottom:2px" > ' + msg + '< / div > < div > < / div > < / div > ');
Q('xmsg').scrollTop = Q('xmsg').scrollHeight;
}
2018-12-16 03:17:26 -05:00
2018-12-17 03:32:08 -05:00
function displayLocalVideo(active) { QV('localVideo', active); adjustVideoWindows(); }
function displayRemoteVideo(active) { QV('remoteVideo', active); adjustVideoWindows(); }
function adjustVideoWindows() {
//var lv = (QS('localVideo')['display'] != 'none');
var rv = (QS('remoteVideo')['display'] != 'none');
QS('localVideo')['top'] = rv ? '320px' : '45px';
}
2018-12-16 16:44:49 -05:00
2018-12-16 03:17:26 -05:00
// Display a message from the remote user
function displayRemote(msg) {
2018-12-17 03:32:08 -05:00
QA('xmsg', '< div style = "clear:both" > < div class = "remoteBubble" > ' + msg + '< / div > < div > < / div > < / div > ');
2018-11-29 20:59:29 -05:00
Q('xmsg').scrollTop = Q('xmsg').scrollHeight;
}
2018-12-16 03:17:26 -05:00
// Display and send a message from the local user
2018-11-29 20:59:29 -05:00
function xsend(event) {
var outtext = Q('xouttext').value;
if (outtext.length > 0) {
Q('xouttext').value = '';
2018-12-17 03:32:08 -05:00
QA('xmsg', '< div style = "clear:both" > < div class = "localBubble" > ' + outtext + '< / div > < div > < / div > < / div > ');
2018-11-29 20:59:29 -05:00
Q('xmsg').scrollTop = Q('xmsg').scrollHeight;
2018-12-16 03:17:26 -05:00
send({ action: 'chat', msg: outtext });
2018-11-29 20:59:29 -05:00
}
}
function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
function parseUriArgs() { var name, r = {}, parsedUri = window.document.location.href.split(/[\?&|\=]/); parsedUri.splice(0, 1); for (x in parsedUri) { switch (x % 2) { case 0: { name = parsedUri[x]; break; } case 1: { r[name] = parsedUri[x]; var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } break; } } } return r; }
2018-12-17 03:32:08 -05:00
// Update user controls
function updateControls() {
QE('sendButton', state == 2);
QE('clearButton', state == 2);
QE('xouttext', state == 2);
QV('camButton', webchannel & & webchannel.ok & & !localStream);
QV('micButton', webchannel & & webchannel.ok & & !localStream);
QV('hangupButton', webchannel & & webchannel.ok & & localStream);
}
2018-12-16 03:17:26 -05:00
// This is the WebRTC setup
2018-12-17 03:32:08 -05:00
function startWebRTC(id, startDataChannel) {
2018-12-16 03:17:26 -05:00
// Setup the WebRTC object
2018-12-17 03:32:08 -05:00
var webrtc;
if (typeof RTCPeerConnection !== 'undefined') { webrtc = new RTCPeerConnection(webrtcconfiguration); }
else if (typeof webkitRTCPeerConnection !== 'undefined') { webrtc = new webkitRTCPeerConnection(webrtcconfiguration); }
webrtc.id = id;
webrtc.onicecandidate = function (e) { try { if (e.candidate != null) { sendws({ action: 'webRtcIce', ice: e.candidate, id: this.id }); } } catch (ex) { } }
webrtc.oniceconnectionstatechange = function () { if (webrtc & & webrtc.iceConnectionState == 'failed') { hangUpButtonClick(webrtc.id); } }
webrtc.ondatachannel = function (ev) {
webchannel = ev.channel;
webchannel.onmessage = function (event) { processMessage(event.data, 2); };
webchannel.onopen = function () { webchannel.ok = true; updateControls(); sendws({ action: 'rtcSwitch', v: 0 }); };
webchannel.onclose = function (event) { if (webchannel & & webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
}
webrtc.onnegotiationneeded = function (event) {
if (webrtc.holdTimer != null) return;
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.
//console.log('onnegotiationneeded', id);
webrtc.holdTimer = null;
webrtc.createOffer(function (offer) { webrtc.setLocalDescription(offer, function () { sendws({ action: 'webRtcSdp', sdp: offer, id: id }); }, function () { hangUpButtonClick(id); }); }, function () { hangUpButtonClick(id); });
}, 20);
}
webrtc.ontrack = function (event) {
//console.log('ontrack', id);
var video = Q('remoteVideoCanvas');
video.srcObject = remoteStream = event.streams[0];
video.onloadedmetadata = function (e) { video.play(); };
displayRemoteVideo(true);
2018-12-16 03:17:26 -05:00
}
2018-12-17 03:32:08 -05:00
//webrtc.onremovetrack = function (event) { console.log('onremovetrack'); }
//webrtc.onicegatheringstatechange = function (event) { console.log('onicegatheringstatechange', event); }
//webrtc.onsignalingstatechange = function (event) { console.log('onsignalingstatechange', event); }
2018-12-16 03:17:26 -05:00
// Initiate the WebRTC offer or handle the offer from the peer.
2018-12-17 03:32:08 -05:00
if (startDataChannel == true) {
2018-12-16 03:17:26 -05:00
webchannel = webrtc.createDataChannel("DataChannel", {}); // { ordered: false, maxRetransmits: 2 }
webchannel.onmessage = function (event) { processMessage(event.data, 2); };
2018-12-17 03:32:08 -05:00
webchannel.onopen = function () { webchannel.ok = true; updateControls(); sendws({ action: 'rtcSwitch', v: 0 }); };
webchannel.onclose = function (event) { if (webchannel & & webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
}
webrtcSessions[id] = webrtc;
return webrtc;
}
function webRtcHandleOffer(id, description) {
//console.log('webRtcHandleOffer', description.sdp.length);
var webrtc = webrtcSessions[id];
if (webrtc) {
2018-12-16 03:17:26 -05:00
webrtc.setRemoteDescription(new RTCSessionDescription(description), function () {
if (description.type == 'offer') {
webrtc.createAnswer(function (answer) {
2018-12-17 03:32:08 -05:00
webrtc.setLocalDescription(answer, function (a, b) {
try { sendws({ action: 'webRtcSdp', sdp: answer, id: id }); } catch (ex) { }
}, function () { hangUpButtonClick(id); });
}, function () { hangUpButtonClick(id); });
2018-12-16 03:17:26 -05:00
}
2018-12-17 03:32:08 -05:00
}, function () { hangUpButtonClick(id); });
2018-12-16 03:17:26 -05:00
}
}
// Indicate to peer that data traffic will no longer be sent over websocket and start holding traffic.
function performWebRtcSwitch() {
if (webchannel & & webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; }
}
// Disconnect everything
function disconnect() {
if (state > 0) { displayControl('Connection closed.'); }
if (state > 1) { setTimeout(start, 500); }
2018-12-17 03:32:08 -05:00
hangUpButtonClick(0, true); // Data channel
hangUpButtonClick(1, true); // Local audio/video
hangUpButtonClick(2, true); // Remote audio/video
if (socket != null) { socket.close(); socket = null; }
updateControls();
2018-12-16 03:17:26 -05:00
state = 0;
}
// Send data over the current transport (WebRTC first)
function send(data) {
if (state != 2) return; // If not in connected state, ignore this.
if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
if (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.
else { if (socket != null) { socket.send(data); } } // If a websocket channel is present, use that.
}
// Send data over the websocket transport (WebSocket only)
function sendws(data) {
if (state != 2) return;
if (typeof data == 'object') { data = JSON.stringify(data); }
if (socket != null) { socket.send(data); }
}
2018-12-17 03:32:08 -05:00
// WebRTC id switcher (0 -> 0, 1 -> 2, 2 -> 1)
function webRtcIdSwitch(id) { if (id == 0) { return 0; } return 3 - id; }
2018-12-16 03:17:26 -05:00
// Process incoming messages
function processMessage(data, transport) {
if (typeof data == 'string') {
try { data = JSON.parse(data); } catch (ex) { console.log('Unable to parse', data); return; }
2018-12-17 03:32:08 -05:00
//console.log(data);
2018-12-16 03:17:26 -05:00
switch (data.action) {
case 'chat': { displayRemote(data.msg); break; } // Incoming chat message.
2018-12-17 03:32:08 -05:00
case 'random': { if (random > data.random) { startWebRTC(0, true); } break; } // If we have a larger random value, we start WebRTC.
case 'webRtcSdp': { if (!webrtcSessions[webRtcIdSwitch(data.id)]) { startWebRTC(webRtcIdSwitch(data.id), false); } webRtcHandleOffer(webRtcIdSwitch(data.id), data.sdp); break; } // Remote WebRTC offer or answer.
case 'webRtcIce': { var webrtc = webrtcSessions[webRtcIdSwitch(data.id)]; if (webrtc) { try { webrtc.addIceCandidate(new RTCIceCandidate(data.ice)); } catch (ex) { } } break; } // Remote ICE candidate
case 'videoStop': { hangUpButtonClick(webRtcIdSwitch(data.id), true); break; }
2018-12-16 03:17:26 -05:00
case 'rtcSwitch': { // WebRTC switch over commands.
switch (data.v) {
case 0: { performWebRtcSwitch(); break; } // Other side is ready for switch over to WebRTC
case 1: { sendws({ action: 'rtcSwitch', v: 2 }); break; } // Other side no longer sending data on websocket, confirm we got the end marker
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
}
break;
}
default: { console.log('Unhandled object data', data); break; }
}
} else {
console.log('Unhandled data', typeof data, data);
}
}
2018-12-16 16:44:49 -05:00
// File sharing button
function fileButtonClick() {
}
// Camera button
function camButtonClick() {
2018-12-17 03:32:08 -05:00
if (localStream == null) { startLocalStream({ video: true, audio: true }); }
2018-12-16 16:44:49 -05:00
}
// Microphone
function micButtonClick() {
2018-12-17 03:32:08 -05:00
if (localStream == null) { startLocalStream({ video: false, audio: true }); }
}
function hangUpButtonClick(id, fromRemote) {
//console.log('hangUpButtonClick', id);
var localVideo = Q('localVideoCanvas');
var remoteVideo = Q('remoteVideoCanvas');
var webrtc = webrtcSessions[1];
if ((id == 0) & & (webchannel != null)) { try { webchannel.close(); } catch (e) { } webchannel = null; }
if (webrtc) {
webrtc.ontrack = null;
webrtc.onremovetrack = null;
webrtc.onremovestream = null;
webrtc.onnicecandidate = null;
webrtc.oniceconnectionstatechange = null;
webrtc.onsignalingstatechange = null;
webrtc.onicegatheringstatechange = null;
webrtc.onnotificationneeded = null;
if ((id == 1) & & localVideo.srcObject) { localVideo.srcObject.getTracks().forEach(track => track.stop()); }
if ((id == 2) & & remoteVideo.srcObject) { remoteVideo.srcObject.getTracks().forEach(track => track.stop()); }
webrtc.close();
delete webrtcSessions[id];
}
if (id == 1) {
localVideo.removeAttribute("src");
localVideo.removeAttribute("srcObject");
if (localStream != null) { localStream = null; }
displayLocalVideo(false);
} else if (id == 2) {
remoteVideo.removeAttribute("src");
remoteVideo.removeAttribute("srcObject");
displayRemoteVideo(false);
}
if (fromRemote != true) { send({ action: 'videoStop', id: id }); }
updateControls();
2018-12-16 16:44:49 -05:00
}
// Setup local audio/video
function startLocalStream(constraints) {
2018-12-17 03:32:08 -05:00
if ((localStream != null) || (webrtcSessions[1] != null)) return;
2018-12-16 16:44:49 -05:00
if (navigator.mediaDevices.getUserMedia) {
localStream = 1;
2018-12-17 03:32:08 -05:00
updateControls();
2018-12-16 16:44:49 -05:00
navigator.mediaDevices.getUserMedia(constraints)
.then(function (stream) {
localStream = stream;
if (constraints.video == true) {
2018-12-17 03:32:08 -05:00
var video = Q('localVideoCanvas'), tracks = localStream.getTracks(), webrtc = startWebRTC(1);
2018-12-16 16:44:49 -05:00
video.srcObject = stream;
video.onloadedmetadata = function (e) { video.play(); };
displayLocalVideo(true);
2018-12-17 03:32:08 -05:00
for (var i in tracks) { webrtc.addTrack(tracks[i], localStream); }
2018-12-16 16:44:49 -05:00
}
})
.catch(function (err) {
2018-12-17 03:32:08 -05:00
displayControl(err.message + '.');
hangUpButtonClick(1);
2018-12-16 16:44:49 -05:00
});
}
}
2018-12-16 03:17:26 -05:00
// This is the main start
2018-12-02 02:41:57 -05:00
function start() {
// Get started
2018-12-17 03:32:08 -05:00
updateControls();
2018-12-02 02:41:57 -05:00
if ((typeof args.id == 'string') & & (args.id.length > 0)) {
socket = new WebSocket(window.location.protocol.replace("http", "ws") + "//" + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/meshrelay.ashx?id=' + args.id);
2018-12-16 03:17:26 -05:00
socket.onopen = function () { state = 1; displayControl('Waiting for other user...'); }
2018-12-02 02:41:57 -05:00
socket.onerror = function (e) { console.error(e); }
2018-12-16 03:17:26 -05:00
socket.onclose = function () { disconnect(); }
2018-12-02 02:41:57 -05:00
socket.onmessage = function (msg) {
2018-12-17 03:32:08 -05:00
if ((state < 2 ) & & ( typeof msg . data = = ' string ' ) & & ( msg . data = = ' c ' ) ) {
hangUpButtonClick(0);
hangUpButtonClick(1);
hangUpButtonClick(2);
2018-12-16 03:17:26 -05:00
displayControl('Connected.');
state = 2;
2018-12-17 03:32:08 -05:00
updateControls();
2018-12-16 03:17:26 -05:00
sendws({ action: 'random', random: random }); // Send a random number. Higher number starts the WebRTC session.
return;
2018-11-29 20:59:29 -05:00
}
2018-12-16 03:17:26 -05:00
if (state == 2) { processMessage(msg.data, 1); }
2018-11-29 20:59:29 -05:00
}
2018-12-02 02:41:57 -05:00
} else {
2018-12-16 03:17:26 -05:00
displayControl('Error: No connection key specified.');
2018-11-29 20:59:29 -05:00
}
}
2018-12-02 02:41:57 -05:00
start();
2018-11-29 20:59:29 -05:00
< / script >
< / body >
< / html >