From caa833c1644f07823d388293210879837eceed6f Mon Sep 17 00:00:00 2001
From: Shadi Akiki <shadi@shadi-HP-dx5150-SFF.(none)>
Date: Wed, 28 Jan 2015 12:38:37 +0200
Subject: [PATCH 1/2] Added multinode capability based on serverless-webrtc.js
 Added export/import of answer/offers (via FileSaver js library) Added
 nicknames (needed for distinguishing different recipients) Added network
 display of recipients + individual send/latency/close buttons Documented in
 README

---
 README.md                         |  23 ++-
 js/FileSaver.min.js               |   7 +
 js/SWGuestConnection.js           |  84 +++++++++
 js/SWHostConnection.js            |  68 ++++++++
 js/serverless-webrtc-multinode.js | 275 ++++++++++++++++++++++++++++++
 serverless-webrtc-multinode.html  | 143 ++++++++++++++++
 6 files changed, 599 insertions(+), 1 deletion(-)
 create mode 100644 js/FileSaver.min.js
 create mode 100644 js/SWGuestConnection.js
 create mode 100644 js/SWHostConnection.js
 create mode 100644 js/serverless-webrtc-multinode.js
 create mode 100644 serverless-webrtc-multinode.html

diff --git a/README.md b/README.md
index c2a7db9..d698655 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,11 @@ WebRTC offer/answer exchange is performed manually by the users, for example
 via IM.  This means that the app can run out of `file:///` directly, without
 involving a web server.  You can send text messages and files between peers.
 
-This repository contains two different clients that can talk to each other:
+This repository contains three different clients that can talk to each other:
 
 1. `serverless-webrtc.js` runs under node.js
 2. `serverless-webrtc.html` runs in Chrome or Firefox
+3. `serverless-webrtc-multinode.html` runs in Chrome or Firefox
 
 Chat is fully interoperable between all of the above (Node, Chrome, Firefox)
 in any combination (tested with Chrome 35 and Firefox 29).
@@ -53,3 +54,23 @@ http://blog.printf.net/articles/2014/07/01/serverless-webrtc-continued
 http://cjb.github.io/serverless-webrtc/serverless-webrtc.html
 
 -- Chris Ball <chris@printf.net> (http://printf.net/)
+
+#### Multi-node capability
+
+serverless-webrtc-multinode.html adds on top of serverless-webrtc.html the functionality of many-to-many connections.
+
+It just does so by making a new WebRTC peer connection for each new pair of users.
+
+Peer connections are either ''Guest'' connections (Bob the recipient), or ''Host'' connections (Alice the sender)
+
+An unordered list (labeled ''Network'') at the bottom of the page shows all the connections made between you and other users.
+
+You could send text to all connected users at once via the ''Send to all'' button, or to a particular user via the ''Send''
+
+button near his/her nickname in the ''Network'' list.
+
+Individual connections can also be closed, or queried for their Round Trip Duration (labeled RTD) via the ''Latency'' button.
+
+RTD is queried by sending an ''echo request'' package, which automatically triggers an ''echo response'' from the other user.
+
+One could optionally use different nicknames for different connections.
diff --git a/js/FileSaver.min.js b/js/FileSaver.min.js
new file mode 100644
index 0000000..e93092f
--- /dev/null
+++ b/js/FileSaver.min.js
@@ -0,0 +1,7 @@
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+var saveAs=saveAs||"undefined"!==typeof navigator&&navigator.msSaveOrOpenBlob&&navigator.msSaveOrOpenBlob.bind(navigator)||function(a){"use strict";if("undefined"===typeof navigator||!/MSIE [1-9]\./.test(navigator.userAgent)){var k=a.document,n=k.createElementNS("http://www.w3.org/1999/xhtml","a"),w="download"in n,x=function(c){var e=k.createEvent("MouseEvents");e.initMouseEvent("click",!0,!1,a,0,0,0,0,0,!1,!1,!1,!1,0,null);c.dispatchEvent(e)},q=a.webkitRequestFileSystem,u=a.requestFileSystem||q||a.mozRequestFileSystem,
+y=function(c){(a.setImmediate||a.setTimeout)(function(){throw c;},0)},r=0,s=function(c){var e=function(){"string"===typeof c?(a.URL||a.webkitURL||a).revokeObjectURL(c):c.remove()};a.chrome?e():setTimeout(e,10)},t=function(c,a,d){a=[].concat(a);for(var b=a.length;b--;){var l=c["on"+a[b]];if("function"===typeof l)try{l.call(c,d||c)}catch(f){y(f)}}},m=function(c,e){var d=this,b=c.type,l=!1,f,p,k=function(){t(d,["writestart","progress","write","writeend"])},g=function(){if(l||!f)f=(a.URL||a.webkitURL||
+a).createObjectURL(c);p?p.location.href=f:void 0==a.open(f,"_blank")&&"undefined"!==typeof safari&&(a.location.href=f);d.readyState=d.DONE;k();s(f)},h=function(a){return function(){if(d.readyState!==d.DONE)return a.apply(this,arguments)}},m={create:!0,exclusive:!1},v;d.readyState=d.INIT;e||(e="download");if(w)f=(a.URL||a.webkitURL||a).createObjectURL(c),n.href=f,n.download=e,x(n),d.readyState=d.DONE,k(),s(f);else{a.chrome&&b&&"application/octet-stream"!==b&&(v=c.slice||c.webkitSlice,c=v.call(c,0,
+c.size,"application/octet-stream"),l=!0);q&&"download"!==e&&(e+=".download");if("application/octet-stream"===b||q)p=a;u?(r+=c.size,u(a.TEMPORARY,r,h(function(a){a.root.getDirectory("saved",m,h(function(a){var b=function(){a.getFile(e,m,h(function(a){a.createWriter(h(function(b){b.onwriteend=function(b){p.location.href=a.toURL();d.readyState=d.DONE;t(d,"writeend",b);s(a)};b.onerror=function(){var a=b.error;a.code!==a.ABORT_ERR&&g()};["writestart","progress","write","abort"].forEach(function(a){b["on"+
+a]=d["on"+a]});b.write(c);d.abort=function(){b.abort();d.readyState=d.DONE};d.readyState=d.WRITING}),g)}),g)};a.getFile(e,{create:!1},h(function(a){a.remove();b()}),h(function(a){a.code===a.NOT_FOUND_ERR?b():g()}))}),g)}),g)):g()}},b=m.prototype;b.abort=function(){this.readyState=this.DONE;t(this,"abort")};b.readyState=b.INIT=0;b.WRITING=1;b.DONE=2;b.error=b.onwritestart=b.onprogress=b.onwrite=b.onabort=b.onerror=b.onwriteend=null;return function(a,b){return new m(a,b)}}}("undefined"!==typeof self&&
+self||"undefined"!==typeof window&&window||this.content);"undefined"!==typeof module&&null!==module?module.exports=saveAs:"undefined"!==typeof define&&null!==define&&null!=define.amd&&define([],function(){return saveAs});
\ No newline at end of file
diff --git a/js/SWGuestConnection.js b/js/SWGuestConnection.js
new file mode 100644
index 0000000..dd16909
--- /dev/null
+++ b/js/SWGuestConnection.js
@@ -0,0 +1,84 @@
+// This is a moderatorless version of
+// http://www.rtcmulticonnection.org/docs/
+
+/* THIS IS BOB, THE ANSWERER/RECEIVER */
+
+function SWGuestConnection() {
+return {
+    username: null,
+    counterparty: null,
+    lastTimestamp: null,
+    latency: null,
+    pc2 : new RTCPeerConnection(cfg, con),
+    dc2 : null,
+    guestConnections: null,
+    p1: null,
+    init: function(un,pc2a2,p11) {
+        var self=this;
+        self.username=un;
+	self.guestConnections=pc2a2;
+	self.p1=p11;
+
+        self.pc2.ondatachannel = function (e) {
+            var fileReceiver2 = new FileReceiver();
+            var datachannel = e.channel || e; // Chrome sends event, FF sends raw channel
+            console.log("Received datachannel (pc2)", arguments);
+            self.dc2 = datachannel;
+            activedc = self.dc2;
+            self.dc2.onopen = function (e) {
+                console.log('data channel connect');
+                $('#waitForConnection').remove();
+                $('#showLocalAnswer').modal('hide');
+                setTimeout(function() { self.send('Initiated'); }, 500);
+                setTimeout(function() { self.send('','echo request'); }, 1500);
+            };
+            self.dc2.onmessage = function (e) { handleOnMessage(e,self); };
+        };
+
+        self.pc2.onsignalingstatechange = onsignalingstatechange;
+        self.pc2.oniceconnectionstatechange = function(e) { oniceconnectionstatechange(e,self) };
+        self.pc2.onicegatheringstatechange = onicegatheringstatechange;
+
+        self.pc2.onaddstream = function (e) {
+            console.log("Got remote stream", e);
+            var el = new Audio();
+            el.autoplay = true;
+            attachMediaStream(el, e.stream);
+        };
+
+        self.pc2.onconnection = handleOnconnection;
+
+	self.pc2.onicecandidate = function (e) {
+	    console.log("ICE candidate (pc2)", e);
+	    if (e.candidate == null)
+	       $('#localAnswer').html(JSON.stringify(self.pc2.localDescription));
+	};
+
+	return self;
+    },
+    send: function(msg,type) {
+        var self=this;
+        self.dc2.send(JSON.stringify({
+            username: self.username,
+            message: msg,
+            timestamp: getTimestamp(),
+	    type: type
+        }));
+    },
+    handleOfferFromPC1:function(offerDesc) {
+        var self=this;
+        self.pc2.setRemoteDescription(offerDesc);
+        self.pc2.createAnswer(function (answerDesc) {
+        self.pc2.setLocalDescription(answerDesc);
+        }, function () { console.warn("No create answer"); });
+    },
+    close: function() {
+	this.pc2.close();
+	if(this.echoId!=null) {
+            clearTimeout(this.echoId);
+            this.echoId=null;
+	}
+    },
+    echoId:null,
+};
+}
diff --git a/js/SWHostConnection.js b/js/SWHostConnection.js
new file mode 100644
index 0000000..cd47f1a
--- /dev/null
+++ b/js/SWHostConnection.js
@@ -0,0 +1,68 @@
+/* THIS IS ALICE, THE CALLER/SENDER */
+
+function SWHostConnection() {
+return {
+    username: null,
+    counterparty: null,
+    lastTimestamp: null,
+    latency: null,
+    pc1 : new RTCPeerConnection(cfg, con),
+    dc1 : null,
+    setupDC1: function() {
+        try {
+        var self=this;
+        self.dc1 = self.pc1.createDataChannel('test', {reliable:true});
+        activedc = self.dc1;
+        console.log("Created datachannel (pc1)");
+	self.dc1.onopen = function (e) {
+	    console.log('data channel connect');
+	    $('#waitForConnection').modal('hide');
+	    $('#waitForConnection').remove();
+	    self.send('Initiated');
+	    setTimeout(function() { self.send('','echo request'); }, 1000);
+	};
+        self.dc1.onmessage = function (e) { handleOnMessage(e,self); };
+        } catch (e) { console.warn("No data channel (pc1)", e); }
+    },
+    createLocalOffer: function() {
+        var self=this;
+        self.setupDC1();
+        self.pc1.createOffer(function (desc) {
+            self.pc1.setLocalDescription(desc, function () {});
+            console.log("created local offer", desc);
+        }, function () {console.warn("Couldn't create offer");});
+    },
+    init: function(un) {
+        var self=this;
+        self.username=un;
+        self.pc1.onconnection = handleOnconnection;
+        self.pc1.onsignalingstatechange = onsignalingstatechange;
+        self.pc1.oniceconnectionstatechange = function(e) { oniceconnectionstatechange(e,self) };
+        self.pc1.onicegatheringstatechange = onicegatheringstatechange;
+	self.pc1.onicecandidate = function (e) {
+	    console.log("ICE candidate (pc1)", e);
+	    if (e.candidate == null) {
+		$('#localOffer').html(JSON.stringify(self.pc1.localDescription));
+	    }
+	};
+	return self;
+    },
+    send: function(msg,type) {
+        var self=this;
+        self.dc1.send(JSON.stringify({
+            username: self.username,
+            message: msg,
+            timestamp: getTimestamp(),
+            type: type
+        }));
+    },
+    close: function() {
+	this.pc1.close();
+	if(this.echoId!=null) {
+            clearTimeout(this.echoId);
+            this.echoId=null;
+	}
+    },
+    echoId:null,
+};
+}
diff --git a/js/serverless-webrtc-multinode.js b/js/serverless-webrtc-multinode.js
new file mode 100644
index 0000000..ba19ecb
--- /dev/null
+++ b/js/serverless-webrtc-multinode.js
@@ -0,0 +1,275 @@
+/* See also:
+    http://www.html5rocks.com/en/tutorials/webrtc/basics/
+    https://code.google.com/p/webrtc-samples/source/browse/trunk/apprtc/index.html
+
+    https://webrtc-demos.appspot.com/html/pc1.html
+*/
+
+$('#sendMessageBtn').click(function() { return sendMessage(); });
+
+var cfg = {"iceServers":[{"url":"stun:23.21.150.121"}]},
+    con = { 'optional': [{'DtlsSrtpKeyAgreement': true}] };
+
+// Since the same JS file contains code for both sides of the connection,
+// activedc tracks which of the two possible datachannel variables we're using.
+var activedc;
+
+$('#showLocalOffer').modal('hide');
+$('#getRemoteAnswer').modal('hide');
+$('#waitForConnection').modal('hide');
+$('#createOrJoin').modal('hide');
+
+$('#addPeer').click(function() {
+	$('#createOrJoin').modal('show');
+});
+
+var hostConnections=[];
+$('#createBtn').click(function() {
+	hostConnections.push(SWHostConnection());
+	hostConnections[hostConnections.length-1].init($('#usernameInput').val());
+
+    $('#showLocalOffer').modal('show');
+    hostConnections[hostConnections.length-1].createLocalOffer();
+});
+
+var guestConnections=[];
+$('#joinBtn').click(function() {
+	guestConnections.push(SWGuestConnection());
+	guestConnections[guestConnections.length-1].init($('#usernameInput').val());
+    $('#getRemoteOffer').modal('show');
+});
+
+$('#offerSentBtn').click(function() {
+    $('#localOffer').html('');
+    $('#getRemoteAnswer').modal('show');
+});
+
+function handleOfferFromPC1(offerDesc,pc2i) {
+    pc2i.handleOfferFromPC1(offerDesc);
+        writeToChatLog("Created local answer", "text-success");
+        console.log("Created local answer: ", offerDesc);
+}
+
+$('#offerRecdBtn').click(function() {
+    var offer = $('#remoteOffer').val();
+    var offerDesc = new RTCSessionDescription(JSON.parse(offer));
+    console.log("Received remote offer", offerDesc);
+    writeToChatLog("Received remote offer", "text-success");
+    handleOfferFromPC1(offerDesc,guestConnections[guestConnections.length-1]);
+    $('#remoteOffer').val('');
+    $('#showLocalAnswer').modal('show');
+});
+
+$('#answerSentBtn').click(function() {
+    $('#localAnswer').html('');
+    $('#waitForConnection').modal('show');
+});
+
+function handleAnswerFromPC2(answerDesc,pci) {
+    console.log("Received remote answer: ", answerDesc);
+    writeToChatLog("Received remote answer", "text-success");
+    pci.setRemoteDescription(answerDesc);
+}
+
+$('#answerRecdBtn').click(function() {
+    var answer = $('#remoteAnswer').val();
+    var answerDesc = new RTCSessionDescription(JSON.parse(answer));
+    handleAnswerFromPC2(answerDesc,hostConnections[hostConnections.length-1].pc1);
+    $('#remoteAnswer').val('');
+    $('#waitForConnection').modal('show');
+});
+
+$('#fileBtn').change(function() {
+    var file = this.files[0];
+    console.log(file);
+
+    sendFile(file);
+});
+
+function fileSent(file) {
+    console.log(file + " sent");
+}
+
+function fileProgress(file) {
+    console.log(file + " progress");
+}
+
+function sendFile(data) {
+    if (data.size) {
+        FileSender.send({
+          file: data,
+          onFileSent: fileSent,
+          onFileProgress: fileProgress,
+        });
+    }
+}
+
+function sendMessage() {
+    if ($('#messageTextBox').val()) {
+       /*var channel = new RTCMultiSession();
+       channel.send({message: $('#messageTextBox').val()});*/
+	for(var i=0;i<hostConnections.length;i++) {
+		hostConnections[i].send($('#messageTextBox').val());
+	}
+	for(var i=0;i<guestConnections.length;i++) {
+		guestConnections[i].send($('#messageTextBox').val());
+	}
+
+	writeToChatLog('Me: '+$('#messageTextBox').val(), "text-success");
+	$('#messageTextBox').val("");
+	// Scroll chat text area to the bottom on new input.
+	$('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
+    }
+
+    return false;
+};
+
+
+function handleOnconnection() {
+    console.log("Datachannel connected");
+    writeToChatLog("Datachannel connected", "text-success");
+    $('#waitForConnection').modal('hide');
+    // If we didn't call remove() here, there would be a race on pc2:
+    //   - first onconnection() hides the dialog, then someone clicks
+    //     on answerSentBtn which shows it, and it stays shown forever.
+    $('#waitForConnection').remove();
+    $('#showLocalAnswer').modal('hide');
+    $('#messageTextBox').focus();
+}
+
+function onsignalingstatechange(state) {
+    console.info('signaling state change:', state);
+}
+
+function oniceconnectionstatechange(state,self) {
+    console.info('ice connection state change:', state);
+    writeToChatLog('Connection update: '+self.username+' x '+self.counterparty+' closed');
+}
+
+function onicegatheringstatechange(state) {
+    console.info('ice gathering state change:', state);
+}
+
+function getTimestamp() {
+    var totalSec = new Date().getTime() / 1000;
+    var hours = parseInt(totalSec / 3600) % 24;
+    var minutes = parseInt(totalSec / 60) % 60;
+    var seconds = parseInt(totalSec % 60);
+
+    var result = (hours < 10 ? "0" + hours : hours) + ":" +
+                 (minutes < 10 ? "0" + minutes : minutes) + ":" +
+                 (seconds  < 10 ? "0" + seconds : seconds);
+
+    return result;
+}
+
+function writeToChatLog(message, message_type) {
+    document.getElementById('chatlog').innerHTML += '<p class=\"' + message_type + '\">' + "[" + getTimestamp() + "] " + message + '</p>';
+                    // Scroll chat text area to the bottom on new input.
+                    $('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
+}
+
+function summarizeRTC1(x) { return {
+	username: x.username,
+	counterparty: x.counterparty,
+	local: x.pc1.localDescription,
+	remote: x.pc1.remoteDescription
+}; }
+
+function summarizeRTC2(x) { return {
+	username: x.username,
+	counterparty: x.counterparty,
+	local: x.pc2.localDescription,
+	remote: x.pc2.remoteDescription
+}; }
+
+$('#exportOffer').click(function() {
+var blob = new Blob([$('#localOffer').val()], {type: "application/json"});
+saveAs(blob, "offer.json");
+});
+
+$('#exportAnswer').click(function() {
+var blob = new Blob([$('#localAnswer').val()], {type: "application/json"});
+saveAs(blob, "answer.json");
+});
+
+function importOfferAnswer(fr,elem) {
+    var file = fr.files[0];
+
+    var reader = new window.FileReader();
+    reader.onload = function(event) {
+	if (event) {
+		$(elem).val(reader.result);
+	}
+    }
+    reader.readAsText(file);
+}
+$('#importOffer').change(function() { importOfferAnswer(this,'#remoteOffer'); });
+$('#importAnswer').change(function() { importOfferAnswer(this,'#remoteAnswer'); });
+
+
+function handleOnMessage(e,self) {
+            console.log("Got message", e.data);
+        var fileReceiver1 = new FileReceiver();
+            if (e.data.size) {
+                fileReceiver1.receive(e.data, {});
+            }
+            else {   
+                 if (e.data.charCodeAt(0) == 2) {
+                    // The first message we get from Firefox (but not Chrome)
+                    // is literal ASCII 2 and I don't understand why -- if we
+                    // leave it in, JSON.parse() will barf.
+                    return;
+                 }
+                 console.log(e);
+                 var data = JSON.parse(e.data);
+                 switch(data.type) {
+                 case 'echo request':
+                     self.send(data.timestamp,'echo response');
+                     break; 
+                 case 'echo response':
+                     self.latency=getTimestamp()+' - '+data.timestamp+' -  '+data.message;
+                     writeToChatLog("RTD: "+self.username+' x '+self.counterparty+' , '+self.latency);
+                     if(self.echoId!=null) {
+                         clearTimeout(self.echoId);
+                         self.echoId=null;
+                     }
+                     break;
+                 default:
+                     // set the counterparty name
+                     if(self.counterparty==null) {
+                         self.counterparty=data.username;
+                         $('#onlineUsers').append(
+                            $('<li>')
+                            .append(self.username+' x '+self.counterparty+'&nbsp;')
+                            .append( $('<button name="latency">').append('Latency') )
+                            .delegate('button[name="latency"]','click', function() {
+                                 writeToChatLog('RTD: '+self.username+' x '+self.counterparty+' ...');
+                                 self.send('','echo request');
+                                 self.echoId=setTimeout(function() { writeToChatLog('RTD: '+self.username+' x '+self.counterparty+' &gt; 30 secs ... closed?'); }, 30000); // timeout in 30 seconds
+                            } )
+                            .append( $('<button name="close">').append('Close') )
+                            .delegate('button[name="close"]','click', function() { self.close(); $(this).parent().remove(); delete self; } )
+                            .append( $('<button name="send">').append('Send') )
+                            .delegate('button[name="send"]','click', function() {
+				if($('#messageTextBox').val()) {
+					self.send($('#messageTextBox').val());
+					writeToChatLog('Me x '+self.counterparty+': '+$('#messageTextBox').val(), "text-success");
+					$('#messageTextBox').val("");
+					// Scroll chat text area to the bottom on new input.
+					$('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
+				}
+			    } )
+                         );
+                     }
+                     self.lastTimestamp=getTimestamp();
+                     // write out message
+                     writeToChatLog(self.counterparty+' x '+self.username+" : "+data.message, "text-info");
+                 }  
+             }
+}
+
+
+//--------------
+$('#createOrJoin').modal('show');
+
diff --git a/serverless-webrtc-multinode.html b/serverless-webrtc-multinode.html
new file mode 100644
index 0000000..725553a
--- /dev/null
+++ b/serverless-webrtc-multinode.html
@@ -0,0 +1,143 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>WebRTC p2p data</title>
+    <script src="js/adapter.js"></script>
+    <script src="js/jquery-2.0.0.js"></script>
+    <script src="js/bootstrap.js"></script>
+    <link href="css/bootstrap.css" rel="stylesheet">
+    <link href="css/bootstrap-responsive.css" rel="stylesheet">
+    <link href="css/serverless-webrtc-bootstrap.css" rel="stylesheet">
+    <script src="js/FileSaver.min.js"></script>
+
+</head>
+<body>
+
+<div class="span12">
+  <fieldset class="well">
+    <p class="head muted">
+      Serverless WebRTC chat demonstration.
+    </p>
+    <div class="text-info" id="chatlog" style="height:350px; overflow:auto;">
+    </div>
+  </fieldset>
+  <form class="form-inline" onSubmit="return false;" action="">
+    <input type="text" id="messageTextBox" placeholder="Type your message here">
+    <button type="submit" id="sendMessageBtn" class="btn">Send to all</button>
+
+	<br>
+	Network:
+	<button class="btn" id="addPeer" data-dismiss="modal" aria-hidden="true">Add</button>
+	<br>
+	<ol id="onlineUsers">
+	<!-- <li><radio name="onlineUser" value="ou1">ou1</li> -->
+	</ol>
+
+  </form>
+  <input type="file" id="fileBtn">
+</div>
+
+<div class="modal" id="showLocalOffer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Send your local offer to someone else</h3>
+  </div>
+  <div class="modal-body">
+    Here's your "offer" -- it tells someone else how to connect to you.  Send the whole thing to them, for example in an instant message or e-mail.
+  <br/>
+  <table border=0>
+  <tr valign="top">
+  <td><textarea class="input-large" id="localOffer" name="localOffer" rows="10" cols="100"></textarea></td>
+  <td><button id="exportOffer">Export</button></td>
+  </tr>
+  </table>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" id="offerSentBtn" data-dismiss="modal" aria-hidden="true">Okay, I sent it.</button>
+  </div>
+</div>
+
+<div class="modal" id="showLocalAnswer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Send your local answer to someone else</h3>
+  </div>
+  <div class="modal-body">
+    Here's your "answer" -- it tells someone else how to connect to you.  Send the whole thing to them, for example in an instant message or e-mail.
+  <br/>
+  <textarea class="input-large" id="localAnswer" name="localAnswer" rows="10" cols="100"></textarea>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" id="answerSentBtn" data-dismiss="modal" aria-hidden="true">Okay, I sent it.</button>
+	<button id="exportAnswer">Export</button>
+  </div>
+</div>
+
+<div class="modal" id="getRemoteOffer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Paste the "offer" you received</h3>
+  </div>
+  <div class="modal-body">
+    The person who created the room will send you an "offer" string -- paste it here.
+  <br/>
+  <table border=0>
+  <tr valign="top">
+  <td><textarea class="input-large" id="remoteOffer" name="remoteOffer" rows="10" cols="100"></textarea></td>
+  <td>Import:<br><input type="file" id="importOffer"></td>
+  </tr>
+  </table>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" id="offerRecdBtn" data-dismiss="modal" aria-hidden="true">Okay, I pasted it.</button>
+  </div>
+</div>
+
+<div class="modal" id="getRemoteAnswer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Paste the "answer" you received</h3>
+  </div>
+  <div class="modal-body">
+    Now paste in the "answer" that was sent back to you.
+  <br/>
+  <table border=0>
+  <tr valign="top">
+  <td><textarea class="input-large" id="remoteAnswer" name="remoteAnswer" rows="10" cols="100"></textarea></td>
+  <td>Import:<br><input type="file" id="importAnswer"></td>
+  </tr>
+  </table>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" id="answerRecdBtn" data-dismiss="modal" aria-hidden="true">Okay, I pasted it.</button>
+  </div>
+</div>
+
+<div class="modal" id="waitForConnection" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Waiting for connection</h3>
+  </div>
+  <div class="modal-body">
+    This dialog will disappear when a connection is made.
+  </div>
+  <div class="spinner" align="center">
+    <img src="img/spinner.gif"></img>
+  </div>
+</div>
+
+<div class="modal" id="createOrJoin" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
+  <div class="modal-header">
+    <h3 id="myModalLabel">Create or join a room?</h3>
+  </div>
+  <div class="modal-footer">
+	<input type="text" id="usernameInput" placeholder="Nickname" />
+    <button class="btn" id="joinBtn" data-dismiss="modal" aria-hidden="true">Join</button>
+    <button class="btn btn-primary" id="createBtn" data-dismiss="modal" aria-hidden="true">Create</button>
+	<br>
+  </div>
+</div>
+
+
+<script src="js/serverless-webrtc-multinode.js"></script>
+<script src="js/file-transfer.js"></script>
+<script src="js/SWHostConnection.js"></script>
+<script src="js/SWGuestConnection.js"></script>
+</body>
+</html>

From c652c3f270b7d1fc4b26296f54899f61a93acd24 Mon Sep 17 00:00:00 2001
From: Shadi Akiki <shadi@shadi-HP-dx5150-SFF.(none)>
Date: Wed, 28 Jan 2015 12:51:01 +0200
Subject: [PATCH 2/2] Noted that sending a file would only send it to the most
 recent connection made

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index d698655..6780840 100644
--- a/README.md
+++ b/README.md
@@ -74,3 +74,5 @@ Individual connections can also be closed, or queried for their Round Trip Durat
 RTD is queried by sending an ''echo request'' package, which automatically triggers an ''echo response'' from the other user.
 
 One could optionally use different nicknames for different connections.
+
+At the moment, sending a file would transmit it only to the last connection made