Browse Source

move all the things

master
Jake Champion 5 years ago
parent
commit
abb9c3a331
17 changed files with 2365 additions and 221 deletions
  1. +4
    -3
      index.html
  2. +28
    -0
      index2.html
  3. +74
    -0
      js/app.js
  4. +9
    -0
      js/build.js
  5. +1057
    -0
      js/bundle.js
  6. +216
    -217
      js/data-connection.js
  7. +0
    -0
      js/firebase.js
  8. +194
    -0
      js/modules/answer.js
  9. +64
    -0
      js/modules/dataConnection.js
  10. +66
    -0
      js/modules/fileReceiver.js
  11. +188
    -0
      js/modules/offer.js
  12. +358
    -0
      js/modules/signaler.js
  13. +20
    -0
      js/modules/textReceiver.js
  14. +60
    -0
      js/modules/textSender.js
  15. +3
    -0
      js/modules/token.js
  16. +21
    -0
      js/modules/utils.js
  17. +3
    -1
      js/new-app.js

+ 4
- 3
index.html View File

@@ -4,8 +4,8 @@
<title>WebRTC Text Chat</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<script src="./firebase.js"> </script>
<script src="./data-connection.js"> </script>
<script src="./js/firebase.js"> </script>
<script src="./js/data-connection.js"> </script>
</head>
<body>
<table style="border-left: 1px solid black; border-right: 1px solid black; width: 100%;">
@@ -22,6 +22,7 @@
</table>
<ul id="playlist">
</ul>
<script src="app.js"></script>
<!-- <script src="bundle.js"></script> -->
<script src="./js/app.js"></script>
</body>
</html>

+ 28
- 0
index2.html View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebRTC Text Chat</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<script src="./js/firebase.js"> </script>
<script src="./js/data-connection.js"> </script>
</head>
<body>
<table style="border-left: 1px solid black; border-right: 1px solid black; width: 100%;">
<tr>
<td>
<div id="chat-output">
</div>
<input type="text" id="user-id" style="font-size: 1.2em; margin-right: 0; width: 5em;"
placeholder="all" title="Enter user-id to send direct messages.">
<input type="text" id="chat-input" style="font-size: 1.2em; margin-left: -.5em; width: 80%;"
placeholder="chat message">
</td>
</tr>
</table>
<ul id="playlist">
</ul>
<script src="./js/bundle.js"></script>
<!-- // <script src="./js/app.js"></script> -->
</body>
</html>

+ 74
- 0
js/app.js View File

@@ -0,0 +1,74 @@
//TODO: Lock down to a specific 'chatroom' -> E.G. We should not need the 'share this link' fuckery
var roomid = 'soft335';
var connection = new DataConnection(roomid);
var playlist = document.getElementById('playlist');

// on data connection opens
connection.onopen = function(e) {
alert('Data connection opened between you and ' + e.userid);
};

// on text message or data object received
connection.onmessage = function(message, userid) {
clearPlaylist();
JsonToPlaylist(message);
};

// on data connection error
connection.onerror = function(e) {
console.log('Error in data connection. Target user id', e.userid, 'Error', e);
};

// on data connection close
connection.onclose = function(e) {
console.log('Data connection closed. Target user id', e.userid, 'Error', e);
};

// using firebase for signaling
connection.firebase = 'signaling';

// check pre-created data connections
connection.check(roomid);
// connection.setup(roomid);

var chatInput = document.getElementById('chat-input');
chatInput.onkeypress = function(e) {
if (e.keyCode !== 13 || !this.value) return;

addToPlaylist(this.value);

connection.send(playlistToJson());

this.value = '';
this.focus();
};

function JsonToPlaylist(message) {
var tracks = JSON.parse(message);
tracks.forEach(function(track) {
addToPlaylist(track);
});
};

function clearPlaylist(){
var playlist = document.getElementById('playlist');
while (playlist.firstChild) playlist.removeChild(playlist.firstChild);
}

function addToPlaylist(message) {
var li = document.createElement('li');
li.classList.add('track');
li.innerHTML = message;
playlist.appendChild(li);
};

function playlistToArray() {
var tracks = Array.prototype.slice.call(document.getElementsByClassName('track'));
return tracks.map(function(track) {
return track.textContent;
});
}

function playlistToJson() {
return JSON.stringify(playlistToArray());
}

+ 9
- 0
js/build.js View File

@@ -0,0 +1,9 @@
var fs = require("fs");
var browserify = require("browserify");
var to5Browserify = require("6to5-browserify");
browserify({ debug: true })
.transform(to5Browserify)
.require("./new-app.js", { entry: true })
.bundle()
.on("error", function (err) { console.log("Error : " + err.message); })
.pipe(fs.createWriteStream("bundle.js"));

+ 1057
- 0
js/bundle.js
File diff suppressed because it is too large
View File


data-connection.js → js/data-connection.js View File

@@ -70,6 +70,30 @@
// it is a backbone object

function Signaler(root) {
// if someone closes the window or tab
window.onbeforeunload = function() {
leave();
// return 'You left the session.';
};

// if someone press "F5" key to refresh the page
window.onkeyup = function(e) {
if (e.keyCode == 116)
leave();
};

// currently you can't eject any user
// however, you can leave the entire session
root.eject = root.leave = function(_userid) {
if (!_userid) return leave();

// broadcaster can throw any user out of the room
signaler.broadcaster && signaler.signal({
getOut: true,
who: _userid
});
};

// unique session-id
var channel = root.channel;

@@ -85,29 +109,128 @@
// object to store all connected participants's ids
var participants = { };

function onSocketMessage(data) {
// don't get self-sent data
if (data.userid == userid) return false;
// it is passed over Offer/Answer objects for reusability
var options = {
onsdp: function(e) {
signaler.signal({
sdp: e.sdp,
to: e.userid
});
},
onicecandidate: function(e) {
signaler.signal({
candidate: e.candidate,
to: e.userid
});
},
onopen: function(e) {
if (root.onopen) root.onopen(e);

// if it is not a leaving alert
if (!data.leaving) return signaler.onmessage(data);
if (!root.channels) root.channels = { };
root.channels[e.userid] = {
send: function(message) {
root.send(message, this.channel);
},
channel: e.channel
};

forwardParticipant(e);
},
onmessage: function(e) {
var message = e.data;
if (!message.size)
message = JSON.parse(message);

if (message.type == 'text')
textReceiver.receive({
data: message,
root: root,
userid: e.userid
});

root.onleave && root.onleave({
userid: data.userid
});
else if (message.size || message.type == 'file')
fileReceiver.receive({
data: message,
root: root,
userid: e.userid
});
else if (root.onmessage)
root.onmessage(message, e.userid);
},
onclose: function(e) {
if (root.onclose) root.onclose(e);

if (data.broadcaster && data.forceClosingTheEntireSession) leave();
var myChannels = root.channels,
closedChannel = e.currentTarget;

// closing peer connection
var peer = peers[data.userid];
if (peer && peer.peer) {
try {
peer.peer.close();
} catch(e) {
for (var _userid in myChannels) {
if (closedChannel === myChannels[_userid].channel)
delete root.channels[_userid];
}
delete peers[data.userid];
}

console.error('DataChannel closed', e);
},
onerror: function(e) {
if (root.onerror) root.onerror(e);

console.error('DataChannel error', e);
},
bandwidth: root.bandwidth
};

var textReceiver = new TextReceiver();
var fileReceiver = new FileReceiver();

// if someone leaves by clicking a "_blank" link
var anchors = document.querySelectorAll('a'),
length = anchors.length;
for (var i = 0; i < length; i++) {
var a = anchors[i];
if (a.href.indexOf('#') !== 0 && a.getAttribute('target') != '_blank')
a.onclick = function() {
leave();
};
}

// signaling implementation
// if no custom signaling channel is provided; use Firebase
if (!root.openSignalingChannel) {
if (!window.Firebase) throw 'You must link <https://cdn.firebase.com/v0/firebase.js> file.';

// Firebase is capable to store data in JSON format
// root.transmitRoomOnce = true;
var socket = new window.Firebase('https://' + (root.firebase || 'chat') + '.firebaseIO.com/' + channel);
socket.on('child_added', function(snap) {
var data = snap.val();
onSocketMessage(data);

// we want socket.io behavior;
// that's why data is removed from firebase servers
// as soon as it is received
if (data.userid != userid) snap.ref().remove();
});

// method to signal the data
this.signal = function(data) {
data.userid = userid;

// "set" allow us overwrite old data
// it is suggested to use "set" however preferred "push"!
socket.push(data);
};
} else {
// custom signaling implementations
// e.g. WebSocket, Socket.io, SignalR, WebSync, HTTP-based POST/GET, Long-Polling etc.
var socket = root.openSignalingChannel(function(message) {
message = JSON.parse(message);
onSocketMessage(message);
});

// method to signal the data
this.signal = function(data) {
data.userid = userid;
socket.send(JSON.stringify(data));
};
}

// it is called when your signaling implementation fires "onmessage"
@@ -165,6 +288,82 @@
if (message.getOut && message.who == userid) leave();
};

this.onice = function(message) {
var peer = peers[message.userid];
if (peer) peer.addIceCandidate(message.candidate);
};

// if someone shared SDP
this.onsdp = function(message) {
var sdp = message.sdp;

if (sdp.type == 'offer') {
var _options = merge(options, {
to: message.userid,
sdp: sdp
});
peers[message.userid] = Answer.createAnswer(_options);
}

if (sdp.type == 'answer') {
peers[message.userid].setRemoteDescription(sdp);
}
};

// call only for session initiator
this.broadcast = function(_config) {
_config = _config || { };
signaler.roomid = _config.roomid || getToken();
signaler.isbroadcaster = true;
(function transmit() {
signaler.signal({
roomid: signaler.roomid,
broadcasting: true
});

!root.transmitRoomOnce && !signaler.left && setTimeout(transmit, root.interval || 3000);
})();

// if broadcaster leaves; clear all JSON files from Firebase servers
if (socket.onDisconnect) socket.onDisconnect().remove();
};

// called for each new participant
this.join = function(_config) {
debugger;
signaler.roomid = _config.roomid;
this.signal({
participationRequest: true,
to: _config.to
});
signaler.sentParticipationRequest = true;
};

function onSocketMessage(data) {
// don't get self-sent data
if (data.userid == userid) return false;

// if it is not a leaving alert
if (!data.leaving) return signaler.onmessage(data);


root.onleave && root.onleave({
userid: data.userid
});

if (data.broadcaster && data.forceClosingTheEntireSession) leave();

// closing peer connection
var peer = peers[data.userid];
if (peer && peer.peer) {
try {
peer.peer.close();
} catch(e) {
}
delete peers[data.userid];
}
}

function participationRequest(message) {
// it is appeared that 10 or more users can send
// participation requests concurrently
@@ -183,7 +382,6 @@
}

// reusable function to create new offer

function createOffer(message) {
var _options = merge(options, {
to: message.userid
@@ -192,7 +390,6 @@
}

// reusable function to create new offer repeatedly

function repeatedlyCreateOffer() {
console.log('signaler.participants', signaler.participants);
var firstParticipant = signaler.participants[0];
@@ -212,97 +409,6 @@
}, 5000);
}

this.onice = function(message) {
var peer = peers[message.userid];
if (peer) peer.addIceCandidate(message.candidate);
};

// if someone shared SDP
this.onsdp = function(message) {
var sdp = message.sdp;

if (sdp.type == 'offer') {
var _options = merge(options, {
to: message.userid,
sdp: sdp
});
peers[message.userid] = Answer.createAnswer(_options);
}

if (sdp.type == 'answer') {
peers[message.userid].setRemoteDescription(sdp);
}
};

// it is passed over Offer/Answer objects for reusability
var options = {
onsdp: function(e) {
signaler.signal({
sdp: e.sdp,
to: e.userid
});
},
onicecandidate: function(e) {
signaler.signal({
candidate: e.candidate,
to: e.userid
});
},
onopen: function(e) {
if (root.onopen) root.onopen(e);

if (!root.channels) root.channels = { };
root.channels[e.userid] = {
send: function(message) {
root.send(message, this.channel);
},
channel: e.channel
};

forwardParticipant(e);
},
onmessage: function(e) {
var message = e.data;
if (!message.size)
message = JSON.parse(message);

if (message.type == 'text')
textReceiver.receive({
data: message,
root: root,
userid: e.userid
});

else if (message.size || message.type == 'file')
fileReceiver.receive({
data: message,
root: root,
userid: e.userid
});
else if (root.onmessage)
root.onmessage(message, e.userid);
},
onclose: function(e) {
if (root.onclose) root.onclose(e);

var myChannels = root.channels,
closedChannel = e.currentTarget;

for (var _userid in myChannels) {
if (closedChannel === myChannels[_userid].channel)
delete root.channels[_userid];
}

console.error('DataChannel closed', e);
},
onerror: function(e) {
if (root.onerror) root.onerror(e);

console.error('DataChannel error', e);
},
bandwidth: root.bandwidth
};

function forwardParticipant(e) {
// for multi-users connectivity
// i.e. video-conferencing
@@ -313,37 +419,6 @@
});
}

var textReceiver = new TextReceiver();
var fileReceiver = new FileReceiver();

// call only for session initiator
this.broadcast = function(_config) {
_config = _config || { };
signaler.roomid = _config.roomid || getToken();
signaler.isbroadcaster = true;
(function transmit() {
signaler.signal({
roomid: signaler.roomid,
broadcasting: true
});

!root.transmitRoomOnce && !signaler.left && setTimeout(transmit, root.interval || 3000);
})();

// if broadcaster leaves; clear all JSON files from Firebase servers
if (socket.onDisconnect) socket.onDisconnect().remove();
};

// called for each new participant
this.join = function(_config) {
signaler.roomid = _config.roomid;
this.signal({
participationRequest: true,
to: _config.to
});
signaler.sentParticipationRequest = true;
};

function leave() {
if (socket.remove) socket.remove();

@@ -383,82 +458,6 @@
// so, he can join other rooms without page reload
root.detectedRoom = false;
}

// currently you can't eject any user
// however, you can leave the entire session
root.eject = root.leave = function(_userid) {
if (!_userid) return leave();

// broadcaster can throw any user out of the room
signaler.broadcaster && signaler.signal({
getOut: true,
who: _userid
});
};

// if someone closes the window or tab
window.onbeforeunload = function() {
leave();
// return 'You left the session.';
};

// if someone press "F5" key to refresh the page
window.onkeyup = function(e) {
if (e.keyCode == 116)
leave();
};

// if someone leaves by clicking a "_blank" link
var anchors = document.querySelectorAll('a'),
length = anchors.length;
for (var i = 0; i < length; i++) {
var a = anchors[i];
if (a.href.indexOf('#') !== 0 && a.getAttribute('target') != '_blank')
a.onclick = function() {
leave();
};
}

// signaling implementation
// if no custom signaling channel is provided; use Firebase
if (!root.openSignalingChannel) {
if (!window.Firebase) throw 'You must link <https://cdn.firebase.com/v0/firebase.js> file.';

// Firebase is capable to store data in JSON format
// root.transmitRoomOnce = true;
var socket = new window.Firebase('https://' + (root.firebase || 'chat') + '.firebaseIO.com/' + channel);
socket.on('child_added', function(snap) {
var data = snap.val();
onSocketMessage(data);

// we want socket.io behavior;
// that's why data is removed from firebase servers
// as soon as it is received
if (data.userid != userid) snap.ref().remove();
});

// method to signal the data
this.signal = function(data) {
data.userid = userid;

// "set" allow us overwrite old data
// it is suggested to use "set" however preferred "push"!
socket.push(data);
};
} else {
// custom signaling implementations
// e.g. WebSocket, Socket.io, SignalR, WebSync, HTTP-based POST/GET, Long-Polling etc.
var socket = root.openSignalingChannel(function(message) {
message = JSON.parse(message);
onSocketMessage(message);
});

// method to signal the data
this.signal = function(data) {
data.userid = userid;
socket.send(JSON.stringify(data));
};
}
}

// reusable stuff

firebase.js → js/firebase.js View File


+ 194
- 0
js/modules/answer.js View File

@@ -0,0 +1,194 @@
var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;

navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.URL = window.webkitURL || window.URL;

var isFirefox = !!navigator.mozGetUserMedia;
var isChrome = !!navigator.webkitGetUserMedia;

var STUN = {
url: isChrome ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121'
};

// old TURN syntax
var TURN = {
url: 'turn:homeo@turn.bistri.com:80',
credential: 'homeo'
};

var iceServers = {
iceServers: [STUN]
};

if (isChrome) {
// in chrome M29 and higher
if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28)
TURN = {
url: 'turn:turn.bistri.com:80',
credential: 'homeo',
username: 'homeo'
};

// No STUN to make sure it works all the time!
iceServers.iceServers = [STUN, TURN];
}

var optionalArgument = {
optional: [{
RtpDataChannels: true
}]
};

var offerAnswerConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: isFirefox,
OfferToReceiveVideo: isFirefox
}
};

// RTCDataChannel.createDataChannel(peer, config);
// RTCDataChannel.setChannelEvents(channel, config);
var RTCDataChannel = {
createDataChannel: function(peer, config) {
var channel = peer.createDataChannel('RTCDataChannel', {
reliable: false
});
this.setChannelEvents(channel, config);
},
setChannelEvents: function(channel, config) {
channel.onopen = function() {
config.onopen({
channel: channel,
userid: config.to
});
};

channel.onmessage = function(e) {
config.onmessage({
data: e.data,
userid: config.to
});
};

channel.onclose = function(event) {
config.onclose({
event: event,
userid: config.to
});
};

channel.onerror = function(event) {
config.onerror({
event: event,
userid: config.to
});
};
}
};

function onSdpSuccess() {}

function onSdpError(e) {
console.error('sdp error:', e.name, e.message);
}

function setBandwidth(sdp, bandwidth) {
bandwidth = bandwidth || { };

// remove existing bandwidth lines
sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , '');
sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + (bandwidth.data || 1638400) + '\r\n');

return sdp;
}

function serializeSdp(sessionDescription, config) {
if (isFirefox) return sessionDescription;

var sdp = sessionDescription.sdp;
sdp = setBandwidth(sdp, config.bandwidth);
sessionDescription.sdp = sdp;
return sessionDescription;
}

// var answer = Answer.createAnswer(config);
// answer.setRemoteDescription(sdp);
// answer.addIceCandidate(candidate);
export default {
createAnswer: function(config) {
var peer = new RTCPeerConnection(iceServers, optionalArgument),
channel;

if (isChrome)
RTCDataChannel.createDataChannel(peer, config);
else if (isFirefox) {
peer.ondatachannel = function(event) {
channel = event.channel;
channel.binaryType = 'blob';
RTCDataChannel.setChannelEvents(channel, config);
};

navigator.mozGetUserMedia({
audio: true,
fake: true
}, function(stream) {

peer.addStream(stream);
peer.setRemoteDescription(new RTCSessionDescription(config.sdp), onSdpSuccess, onSdpError);
peer.createAnswer(function(sdp) {
peer.setLocalDescription(sdp);
config.onsdp({
sdp: sdp,
userid: config.to
});
}, onSdpError, offerAnswerConstraints);

}, mediaError);
}

peer.onicecandidate = function(event) {
if (event.candidate) {
config.onicecandidate({
candidate: event.candidate,
userid: config.to
});
}
};

peer.oniceconnectionstatechange = function() {
// "disconnected" state: Liveness checks have failed for one or more components.
// This is more aggressive than failed, and may trigger intermittently
// (and resolve itself without action) on a flaky network.
if (!!peer && peer.iceConnectionState == 'disconnected') {
peer.close();
console.error('iceConnectionState is <disconnected>.');
}
};

if (isChrome) {
peer.setRemoteDescription(new RTCSessionDescription(config.sdp), onSdpSuccess, onSdpError);
peer.createAnswer(function(sdp) {
sdp = serializeSdp(sdp, config);
peer.setLocalDescription(sdp);

config.onsdp({
sdp: sdp,
userid: config.to
});
}, onSdpError, offerAnswerConstraints);
}

this.peer = peer;

return this;
},
addIceCandidate: function(candidate) {
this.peer.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
}));
}
};

+ 64
- 0
js/modules/dataConnection.js View File

@@ -0,0 +1,64 @@
import Signaler from './signaler.js';
import getToken from './token.js';
import TextSender from './textSender.js';

export default class DataConnection {
constructor(channel) {
this.signaler;
this.self = this;
this.channel = channel;
this.userid = getToken();
}

onconnection(room) {
if (this.self.detectedRoom) return;
this.self.detectedRoom = true;

if (this.self._roomid && this.self._roomid != room.roomid) return;

this.self.join(room);
};

setup(roomid) {
this.self.detectedRoom = true;
!this.signaler && this.initSignaler();
this.signaler.broadcast({
roomid: roomid || getToken()
});
};

join(room) {
!this.signaler && this.initSignaler();
this.signaler.join({
to: room.userid,
roomid: room.roomid
});
};

send(data, _channel) {
if (!data) throw 'No file, data or text message to share.';
if (data.size)
FileSender.send({
file: data,
root: self,
channel: _channel,
userid: self.userid
});
else
TextSender.send({
text: data,
root: self,
channel: _channel,
userid: self.userid
});
};

check(roomid) {
self._roomid = roomid;
this.initSignaler();
};

initSignaler() {
this.signaler = new Signaler(this.self);
};
}

+ 66
- 0
js/modules/fileReceiver.js View File

@@ -0,0 +1,66 @@
export default class FileReceiver {
constructor() {
var content = [],
fileName = '',
packets = 0,
numberOfPackets = 0;

this.receive = function(config) {
var root = config.root;
var data = config.data;

if (isFirefox) {
if (data.fileName)
fileName = data.fileName;

if (data.size) {
var reader = new window.FileReader();
reader.readAsDataURL(data);
reader.onload = function(event) {
FileSaver.SaveToDisk({
fileURL: event.target.result,
fileName: fileName
});

if (root.onFileReceived)
root.onFileReceived({
fileName: fileName,
userid: config.userid
});
};
}
}

if (isChrome) {
if (data.packets)
numberOfPackets = packets = parseInt(data.packets);

if (root.onFileProgress)
root.onFileProgress({
packets: {
remaining: packets--,
length: numberOfPackets,
received: numberOfPackets - packets
},
userid: config.userid
});

content.push(data.message);

if (data.last) {
FileSaver.SaveToDisk({
fileURL: content.join(''),
fileName: data.name
});

if (root.onFileReceived)
root.onFileReceived({
fileName: data.name,
userid: config.userid
});
content = [];
}
}
};
}
}

+ 188
- 0
js/modules/offer.js View File

@@ -0,0 +1,188 @@
var RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
var RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
var RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;

navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.URL = window.webkitURL || window.URL;

var isFirefox = !!navigator.mozGetUserMedia;
var isChrome = !!navigator.webkitGetUserMedia;

var STUN = {
url: isChrome ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121'
};

// old TURN syntax
var TURN = {
url: 'turn:homeo@turn.bistri.com:80',
credential: 'homeo'
};

var iceServers = {
iceServers: [STUN]
};

if (isChrome) {
// in chrome M29 and higher
if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28)
TURN = {
url: 'turn:turn.bistri.com:80',
credential: 'homeo',
username: 'homeo'
};

// No STUN to make sure it works all the time!
iceServers.iceServers = [STUN, TURN];
}

var optionalArgument = {
optional: [{
RtpDataChannels: true
}]
};

var offerAnswerConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: isFirefox,
OfferToReceiveVideo: isFirefox
}
};

// RTCDataChannel.createDataChannel(peer, config);
// RTCDataChannel.setChannelEvents(channel, config);
var RTCDataChannel = {
createDataChannel: function(peer, config) {
var channel = peer.createDataChannel('RTCDataChannel', {
reliable: false
});
this.setChannelEvents(channel, config);
},
setChannelEvents: function(channel, config) {
channel.onopen = function() {
config.onopen({
channel: channel,
userid: config.to
});
};

channel.onmessage = function(e) {
config.onmessage({
data: e.data,
userid: config.to
});
};

channel.onclose = function(event) {
config.onclose({
event: event,
userid: config.to
});
};

channel.onerror = function(event) {
config.onerror({
event: event,
userid: config.to
});
};
}
};

function onSdpSuccess() {}

function onSdpError(e) {
console.error('sdp error:', e.name, e.message);
}

function setBandwidth(sdp, bandwidth) {
bandwidth = bandwidth || { };

// remove existing bandwidth lines
sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , '');
sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + (bandwidth.data || 1638400) + '\r\n');

return sdp;
}

function serializeSdp(sessionDescription, config) {
if (isFirefox) return sessionDescription;

var sdp = sessionDescription.sdp;
sdp = setBandwidth(sdp, config.bandwidth);
sessionDescription.sdp = sdp;
return sessionDescription;
}


export default {
createOffer: function(config) {
var peer = new RTCPeerConnection(iceServers, optionalArgument);

RTCDataChannel.createDataChannel(peer, config);

function sdpCallback() {
config.onsdp({
sdp: peer.localDescription,
userid: config.to
});
}

peer.onicecandidate = function(event) {
if (!event.candidate) sdpCallback();
};

peer.ongatheringchange = function(event) {
if (event.currentTarget && event.currentTarget.iceGatheringState === 'complete')
sdpCallback();
};

peer.oniceconnectionstatechange = function() {
// "disconnected" state: Liveness checks have failed for one or more components.
// This is more aggressive than failed, and may trigger intermittently
// (and resolve itself without action) on a flaky network.
if (!!peer && peer.iceConnectionState == 'disconnected') {
peer.close();
console.error('iceConnectionState is <disconnected>.');
}
};

if (isChrome) {
peer.createOffer(function(sdp) {
sdp = serializeSdp(sdp, config);
peer.setLocalDescription(sdp);
}, onSdpError, offerAnswerConstraints);

} else if (isFirefox) {
navigator.mozGetUserMedia({
audio: true,
fake: true
}, function(stream) {
peer.addStream(stream);
peer.createOffer(function(sdp) {
peer.setLocalDescription(sdp);
config.onsdp({
sdp: sdp,
userid: config.to
});
}, onSdpError, offerAnswerConstraints);

}, mediaError);
}

this.peer = peer;

return this;
},
setRemoteDescription: function(sdp) {
this.peer.setRemoteDescription(new RTCSessionDescription(sdp), onSdpSuccess, onSdpError);
},
addIceCandidate: function(candidate) {
this.peer.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
}));
}
};

// export Offer;

+ 358
- 0
js/modules/signaler.js View File

@@ -0,0 +1,358 @@
import getToken from './token.js';
import TextReceiver from './textReceiver.js';
import FileReceiver from './fileReceiver.js';
import Offer from './offer.js';
import Answer from './answer.js';

export default class Signaler {
constructor(root) {
// save reference to root obj
this.root = root;

// if someone closes the window or tab
window.onbeforeunload = function() {
leave();
};

// unique session-id
this.channel = root.channel;

// unique identifier for the current user
this.userid = root.userid || getToken();

// self instance
this.signaler = this;

// object to store all connected peers
this.peers = { };

// object to store all connected participants's ids
this.participants = { };

// it is passed over Offer/Answer objects for reusability
this.options = {
onsdp: function(e) {
this.signaler.signal({
sdp: e.sdp,
to: e.userid
});
},
onicecandidate: function(e) {
this.signaler.signal({
candidate: e.candidate,
to: e.userid
});
},
onopen: function(e) {
if (this.root.onopen) this.root.onopen(e);

if (!this.root.channels) this.root.channels = { };
this.root.channels[e.userid] = {
send: function(message) {
this.root.send(message, this.channel);
},
channel: e.channel
};

forwardParticipant(e);
},
onmessage: function(e) {
var message = e.data;
if (!message.size)
message = JSON.parse(message);

if (message.type == 'text')
this.textReceiver.receive({
data: message,
root: this.root,
userid: e.userid
});

else if (message.size || message.type == 'file')
this.fileReceiver.receive({
data: message,
root: this.root,
userid: e.userid
});
else if (this.root.onmessage)
this.root.onmessage(message, e.userid);
},
onclose: function(e) {
if (this.root.onclose) this.root.onclose(e);

var myChannels = this.root.channels,
closedChannel = e.currentTarget;

for (var _userid in myChannels) {
if (closedChannel === myChannels[_userid].channel)
delete this.root.channels[_userid];
}

console.error('DataChannel closed', e);
},
onerror: function(e) {
if (this.root.onerror) this.root.onerror(e);

console.error('DataChannel error', e);
},
bandwidth: this.root.bandwidth
};

this.textReceiver = new TextReceiver();
this.fileReceiver = new FileReceiver();

// Firebase is capable to store data in JSON format
// root.transmitRoomOnce = true;
this.socket = new window.Firebase('https://' + (this.root.firebase || 'chat') + '.firebaseIO.com/' + this.channel);
this.socket.on('child_added', function(snap) {
var data = snap.val();
onSocketMessage(data);

// we want socket.io behavior;
// that's why data is removed from firebase servers
// as soon as it is received
if (data.userid != this.userid) snap.ref().remove();
});

// method to signal the data
this.signal = function(data) {
data.userid = this.userid;

// "set" allow us overwrite old data
// it is suggested to use "set" however preferred "push"!
this.socket.push(data);
};

// it is called when your signaling implementation fires "onmessage"
this.onmessage = function(message) {
// if new room detected
if (message.roomid && message.broadcasting

// one user can participate in one room at a time
&& !this.signaler.sentParticipationRequest) {

// broadcaster's and participant's session must be identical
this.root.onconnection(message);

} else
// for pretty logging
console.debug(JSON.stringify(message, function(key, value) {
if (value && value.sdp) {
console.log(value.sdp.type, '————', value.sdp.sdp);
return '';
} else return value;
}, '————'));

// if someone shared SDP
if (message.sdp && message.to == this.userid)
this.onsdp(message);

// if someone shared ICE candidate
if (message.candidate && message.to == this.userid)
this.onice(message);

// if someone sent participation request
if (message.participationRequest && message.to == this.userid) {
this.participants[message.userid] = message.userid;
participationRequest(message);
}

// session initiator transmitted new participant's details
// it is useful for multi-users connectivity
if (message.conferencing && message.newcomer != this.userid && !!this.participants[message.newcomer] == false) {
this.participants[message.newcomer] = message.newcomer;
this.signaler.signal({
participationRequest: true,
to: message.newcomer
});
}

// if current user is suggested to play role of broadcaster
// to keep active session all the time; event if session initiator leaves
if (message.playRoleOfBroadcaster === this.userid)
this.broadcast({
roomid: this.signaler.roomid
});

// broadcaster forced the user to leave his room!
if (message.getOut && message.who == this.userid) leave();
};

this.onice = function(message) {
var peer = this.peers[message.userid];
if (peer) peer.addIceCandidate(message.candidate);
};

// if someone shared SDP
this.onsdp = function(message) {
var sdp = message.sdp;

if (sdp.type == 'offer') {
var _options = merge(options, {
to: message.userid,
sdp: sdp
});
this.peers[message.userid] = Answer.createAnswer(_options);
}

if (sdp.type == 'answer') {
this.peers[message.userid].setRemoteDescription(sdp);
}
};

// call only for session initiator
this.broadcast = function(_config) {
_config = _config || { };
this.signaler.roomid = _config.roomid || getToken();
this.signaler.isbroadcaster = true;
(function transmit() {
this.signaler.signal({
roomid: this.signaler.roomid,
broadcasting: true
});

!this.root.transmitRoomOnce && !this.signaler.left && setTimeout(transmit, this.root.interval || 3000);
}.bind(this))();

// if broadcaster leaves; clear all JSON files from Firebase servers
if (this.socket.onDisconnect) this.socket.onDisconnect().remove();
};

// called for each new participant
this.join = function(_config) {
this.signaler.roomid = _config.roomid;
this.signal({
participationRequest: true,
to: _config.to
});
this.signaler.sentParticipationRequest = true;
};

function onSocketMessage(data) {
// don't get self-sent data
if (data.userid == this.userid) return false;

// if it is not a leaving alert
if (!data.leaving) return this.signaler.onmessage(data);


this.root.onleave && this.root.onleave({
userid: data.userid
});

if (data.broadcaster && data.forceClosingTheEntireSession) leave();

// closing peer connection
var peer = this.peers[data.userid];
if (peer && peer.peer) {
try {
peer.peer.close();
} catch(e) {
}
delete this.peers[data.userid];
}
}

function participationRequest(message) {
// it is appeared that 10 or more users can send
// participation requests concurrently
if (!this.signaler.creatingOffer) {
this.signaler.creatingOffer = true;
createOffer(message);
setTimeout(function() {
this.signaler.creatingOffer = false;
if (this.signaler.participants &&
this.signaler.participants.length) repeatedlyCreateOffer();
}, 5000);
} else {
if (!this.signaler.participants) this.signaler.participants = [];
this.signaler.participants[this.signaler.participants.length] = message;
}
}

// reusable function to create new offer
function createOffer(message) {
var _options = merge(this.options, {
to: message.userid
});
this.peers[message.userid] = Offer.createOffer(_options);
}

// reusable function to create new offer repeatedly
function repeatedlyCreateOffer() {
console.log('signaler.participants', signaler.participants);
var firstParticipant = this.signaler.participants[0];
if (!firstParticipant) return;

this.signaler.creatingOffer = true;
createOffer(firstParticipant);

// delete "firstParticipant" and swap array
delete this.signaler.participants[0];
this.signaler.participants = swap(this.signaler.participants);

setTimeout(function() {
this.signaler.creatingOffer = false;
if (this.signaler.participants[0])
repeatedlyCreateOffer();
}, 5000);
}

function forwardParticipant(e) {
// for multi-users connectivity
// i.e. video-conferencing
this.signaler.isbroadcaster &&
this.signaler.signal({
conferencing: true,
newcomer: e.userid
});
}

function leave() {
if (this.socket.remove) this.socket.remove();

this.signaler.signal({
leaving: true,

// is he session initiator?
broadcaster: !!this.signaler.broadcaster,

// is he willing to close the entire session
forceClosingTheEntireSession: !!this.root.autoCloseEntireSession
});

// if broadcaster leaves; don't close the entire session
if (this.signaler.isbroadcaster && !this.root.autoCloseEntireSession) {
var gotFirstParticipant;
for (var participant in this.participants) {
if (gotFirstParticipant) break;
gotFirstParticipant = true;
this.participants[participant] && this.signaler.signal({
playRoleOfBroadcaster: this.participants[participant]
});
}
}

this.participants = { };

// close all connected peers
for (var peer in this.peers) {
peer = this.peers[peer];
if (peer.peer) peer.peer.close();
}
this.peers = { };

this.signaler.left = true;

// so, he can join other rooms without page reload
this.root.detectedRoom = false;
}
}
}

function merge(mergein, mergeto) {
for (var item in mergeto) {
mergein[item] = mergeto[item];
}
return mergein;
}

+ 20
- 0
js/modules/textReceiver.js View File

@@ -0,0 +1,20 @@
export default class TextReceiver {
constructor(){
this.content = [];
}

receive(config) {
var root = config.root;
var data = config.data;

this.content.push(data.message);
if (data.last) {
if (root.onmessage)
root.onmessage({
data: content.join(''),
userid: config.userid
});
this.content = [];
}
}
}

+ 60
- 0
js/modules/textSender.js View File

@@ -0,0 +1,60 @@
var isFirefox = !!navigator.mozGetUserMedia;

export default {
send: function(config) {
var root = config.root;

function send(message) {
message = JSON.stringify(message);

// share data between two unique users i.e. direct messages
if (config.channel) return config.channel.send(message);

// share data with all connected users
var channels = root.channels || { };
for (var channel in channels) {
channels[channel].channel.send(message);
}
}


var initialText = config.text,
packetSize = 1000,
textToTransfer = '';

if (typeof initialText !== 'string')
initialText = JSON.stringify(initialText);

if (isFirefox || initialText.length <= packetSize)
send(config.text);
else
sendText(initialText);

function sendText(textMessage, text) {
var data = {
type: 'text'
};

if (textMessage) {
text = textMessage;
data.packets = parseInt(text.length / packetSize);
}

if (text.length > packetSize)
data.message = text.slice(0, packetSize);
else {
data.message = text;
data.last = true;
}

send(data);

textToTransfer = text.slice(data.message.length);

if (textToTransfer.length)
setTimeout(function() {
sendText(null, textToTransfer);
}, 500);
}
}
};

+ 3
- 0
js/modules/token.js View File

@@ -0,0 +1,3 @@
export default function() {
return Math.round(Math.random() * 60535) + 5000;
}

+ 21
- 0
js/modules/utils.js View File

@@ -0,0 +1,21 @@
// swap arrays

export function swap(arr) {
var swapped = [],
length = arr.length;
for (var i = 0; i < length; i++)
if (arr[i] && arr[i] !== true)
swapped[swapped.length] = arr[i];
return swapped;
}

export function merge(mergein, mergeto) {
for (var item in mergeto) {
mergein[item] = mergeto[item];
}
return mergein;
}

export function mediaError() {
throw 'Unable to get access to fake audio.';
}

app.js → js/new-app.js View File

@@ -1,3 +1,5 @@
import DataConnection from './modules/dataConnection.js';

//TODO: Lock down to a specific 'chatroom' -> E.G. We should not need the 'share this link' fuckery
var roomid = 'soft335';
var connection = new DataConnection(roomid);
@@ -35,7 +37,7 @@ connection.firebase = 'signaling';

// check pre-created data connections
connection.check(roomid);
// connection.setup(roomid);
connection.setup(roomid);

var chatInput = document.getElementById('chat-input');
chatInput.onkeypress = function(e) {

Loading…
Cancel
Save