Skip to content

Instantly share code, notes, and snippets.

@keroserene
Created February 19, 2014 23:27
Show Gist options
  • Save keroserene/9103784 to your computer and use it in GitHub Desktop.
Save keroserene/9103784 to your computer and use it in GitHub Desktop.
SCTP multi-datachannel test
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=ISO-8859-1'>
<title>SCTP multi-datachannels</title>
<style>
body {
background-color: #000; color: #0f0;
text-align: center; margin: auto;
}
textarea { font-family: monospace; }
#logDiv {
font-family: monospace;
white-space: pre;
}
.peer {
vertical-align: top; display: inline-block; min-width: 200px;
padding 30px; margin: 0; height: 400px;
}
#peerconnection {
display: inline-block; position: relative;
width: 50%; min-width: 200px; max-width: 300px;
margin: 0; padding: 0; padding-top: 30px; height: 400px;
border: 1px solid #050;
} #peerconnection h5 { position: absolute; top: 0; margin: 0; padding: 8px;}
.datachannel {
padding: 8px 0; margin: 2px 0; font-size: 11px;
border: 1px solid #888; border-left: 0; border-right: 0;
background-color: #000; cursor: pointer;
} .datachannel:hover { border-color: #aaa; background-color: #111;}
button { padding: 8px; }
.msg {
display: block; background-color: #000; color: #080; resize: none;
outline: none; margin: 8px;
} .msg:hover { color: #0b0; }
#bob-msg { height: 100%; width: 400px; }
</style>
</head>
<body>
<p>
SCTP peer connection ready.<br/>
Click 'Add datachannel' to create a new data channel between Alice and Bob.<br/>
Type a message into Alice's box, then click a data channel to send upon that
single channel.
</p>
<p>
Expected behavior: a single '$channel: $msg' line appearing on Bob's side per
message from Alice.
</p>
<div id='logDiv'>
</div>
<div class='peer'>Alice
<textarea id='alice-msg' class='msg'></textarea>
<div><button onclick="newDataChannel()">Add datachannel</button></div>
</div>
<div id='peerconnection'>
<h5>peer connection</h5>
</div>
<div class='peer'>Bob
<textarea readonly id='bob-msg' class='msg'></textarea>
</div>
<!-- END OF HTML -->
<script>
'strict';
// Generic use of RTCPeerConnection as the name for WebRTC's peer connection
var RTCPC = RTCPC || webkitRTCPeerConnection || mozRTCPeerConnection;
var datachannels = document.getElementById('peerconnection');
var alicemsg = document.getElementById('alice-msg');
var bobmsg = document.getElementById('bob-msg');
window.onload = function () {
// Bind alice and bob together.
alice = new SimpleDataPeer({debug: true, debugPeerName: 'p1_alice'});
bob = new SimpleDataPeer({debug: true, debugPeerName: 'p2_bob'});
alice.setSendSignalMessage(bob.handleSignal.bind(bob));
bob.setSendSignalMessage(alice.handleSignal.bind(alice));
bob.msgCallback = bobReceivesMessage;
}
function newDataChannel() {
var label = 'c' + Math.floor(Math.random() * 1000000);
alice.newDataChannel(label)
var newDc = document.createElement('div');
newDc.className = 'datachannel';
newDc.id = 'dc-' + label;
newDc.innerHTML = label;
newDc.onclick = function() {
var msg = alicemsg.value;
if (!msg) return;
alicemsg.value = '';
alice.send(label, msg);
}
datachannels.appendChild(newDc);
}
function bobReceivesMessage(label, msg) {
var divid = 'dc-' + label;
var div = document.getElementById(divid);
bobmsg.value += label + ': ' + msg + '\n';
}
//-----------------------------------------------------------------------------
// Console debugging utilities
//-----------------------------------------------------------------------------
// Abbreviation for console['type'] (type = log/warn/error) that inserts time at
// front.
function trace_to_console(type) {
var argsArray = [];
for (var i = 1; i < arguments.length; i++) {
argsArray[i - 1] = arguments[i];
}
var text = argsArray[0];
var s = (performance.now() / 1000).toFixed(3) + ': ' + text;
argsArray[0] = s;
console[type].apply(console, argsArray);
}
var trace = {
log: trace_to_console.bind(null, 'log'),
warn: trace_to_console.bind(null, 'warn'),
error: trace_to_console.bind(null, 'error'),
};
/**
* A class that wraps a peer connection and its data channels.
*/
function SimpleDataPeer(options) {
if (options) {
this._debugPeerName = options.debugPeerName;
this._debug = options.debug;
}
// A way to speak to the peer to send SDP headers etc.
this._sendSignalMessage = null;
// The peer connection.
this.pc = new RTCPC(null, {optional: [{DtlsSrtpKeyAgreement: true}]});
// Add basic event handlers.
this.pc.addEventListener('icecandidate',
this._onIceCallback.bind(this));
this.pc.addEventListener('datachannel',
this._onDataChannel.bind(this));
this.pc.addEventListener('negotiationneeded',
this._onNegotiationNeeded.bind(this));
this.pc.addEventListener('signalingstatechange',
this._onSignalingStateChange.bind(this));
// Start off with no open data channels.
this.channels = {};
}
SimpleDataPeer.prototype.setSendSignalMessage = function (sendSignalMessageFn) {
this._sendSignalMessage = sendSignalMessageFn;
}
// Handle a message sent on the signalling channel to this peer.
SimpleDataPeer.prototype.handleSignal = function(message) {
var json = JSON.parse(message);
trace.log(this._debugPeerName + ': handleSignal:\n', json);
if (json.sdp) {
// Set the remote description.
this.pc.setRemoteDescription(
new RTCSessionDescription(json.sdp),
// Success
function () {
trace.log(this._debugPeerName + ': ' +
'setRemoteDescription sucess:', this.pc.remoteDescription);
if (this.pc.remoteDescription.type == 'offer') {
this.pc.createAnswer(this._onDescription.bind(this));
}
}.bind(this),
// Failure
function (e) {
trace.error(this._debugPeerName + ': ' +
'setRemoteDescription failed:', e);
}.bind(this));
} else if (json.candidate) {
// Add remote ice candidate.
this.pc.addIceCandidate(new RTCIceCandidate(json.candidate));
} else {
trace.warn(this._debugPeerName + ': ' +
'handleSignal got unexpected message: ', message);
}
}
// Connect to the peer by the signalling channel.
SimpleDataPeer.prototype.connect = function () {
this.pc.createOffer(
this._onDescription.bind(this),
function(e) {
trace.error(this._debugPeerName + ': ' +
'createOffer failed: ', e.toString());
}.bind(this)
);
}
// Create a new data channel.
SimpleDataPeer.prototype.newDataChannel = function(channelId, options) {
// if no channel id is specified, fail.
if (!channelId) {
trace.error(this._debugPeerName + ': No channelId given.');
return null;
}
// If a channel with this label already exists, fail.
if (channelId in this.channels) {
trace.error(this._debugPeerName + ': channelId already exists: ' + channelId);
return null;
}
// Default to reliable channels.
if (!options) { options = {'reliable': true} }
// Create and setup the new data channel.
var channel = this.pc.createDataChannel(channelId, options);
this._setupDataChannel(channel);
return channel;
}
SimpleDataPeer.prototype.send = function(channelId, message) {
if (!(channelId in this.channels)) {
trace.error(this._debugPeerName + ': ' + 'No such channel id: ' +
channelId);
return;
}
var channel = this.channels[channelId];
try {
channel.send(message);
trace.log(this._debugPeerName + ': channels[' + channel.label + ']:' +
'sent: ', message);
} catch (e) {
trace.error(this._debugPeerName + ': channels[' + channel.label + ']: ' +
'send failed. channel:', channel, '\nException: ', e.toString());
}
}
SimpleDataPeer.prototype.closeChannel = function (channelId) {
this.channels[channelId].close();
delete this.channels[channelId];
}
SimpleDataPeer.prototype.close = function() {
for(var channelId in this.channels) {
this.closeChannel(channelId);
}
this.pc.close();
trace.log(this._debugPeerName + ': Closed peer connection.');
}
// When we get our description, we set it to be our local description and
// send it to the peer.
SimpleDataPeer.prototype._onDescription = function (description) {
if (this._sendSignalMessage) {
this.pc.setLocalDescription(description,
function(message) {
this._sendSignalMessage(JSON.stringify({'sdp':description}));
}.bind(this),
function (e) {
trace.error(this._debugPeerName + ': ' +
'setLocalDescription failed:', e);
}.bind(this));
} else {
trace.error(this._debugPeerName + ': _onDescription: _sendSignalMessage is not set, so we did not set the local description. ');
}
}
SimpleDataPeer.prototype._onNegotiationNeeded = function() {
trace.log(this._debugPeerName + ': _onNegotiationNeeded, connecting...', this.pc);
this.connect();
}
SimpleDataPeer.prototype._onSignalingStateChange = function() {
trace.log(this._debugPeerName + ': _onSignalingStateChange: ', this.pc.signalingState);
}
SimpleDataPeer.prototype._onIceCallback = function(event) {
if (event.candidate) {
// Send IceCandidate to peer.
trace.log(this._debugPeerName + ': ice callback with candidate', event);
if (this._sendSignalMessage) {
this._sendSignalMessage(JSON.stringify({'candidate': event.candidate}));
} else {
trace.warn(this._debugPeerName + ': _onDescription: _sendSignalMessage is not set.');
}
}
}
SimpleDataPeer.prototype._onDataChannel = function(event) { this._setupDataChannel(event.channel); }
SimpleDataPeer.prototype._setupDataChannel = function(channel) {
if (channel.label in this.channels) {
trace.warn('Channel ' + channel.label + ' already exists; closing the old one and creating a new one.');
this.channels[channel.label].close();
}
this.channels[channel.label] = channel;
if (this._debug) {
channel.addEventListener('open',
this._onChannelStateChange.bind(this, channel));
channel.addEventListener('close',
this._onChannelStateChange.bind(this, channel));
channel.addEventListener('error',
this._onChannelStateChange.bind(this, channel));
channel.addEventListener('message',
this._onMessageCallback.bind(this, channel));
}
}
SimpleDataPeer.prototype._onChannelStateChange = function (channel) {
trace.log(this._debugPeerName + ': ' + 'dataChannel(' + channel.label +
'): ' + 'State changed: ' + channel.readyState, event);
}
// Channel event handlers
SimpleDataPeer.prototype._onMessageCallback = function(channel, event) {
trace.log(this._debugPeerName + ': dataChannel(' + channel.label +
'): Received message: ', event.data)
if (this.msgCallback)
this.msgCallback(channel.label, event.data);
}
SimpleDataPeer.prototype.getChannelIds = function () {
return Object.keys(this.channels);
}
SimpleDataPeer.prototype.getChannel = function (channelId) {
return this.channels[channelId];
}
SimpleDataPeer.prototype.removeChannel = function (channelId) {
delete this.channels[channelId];
}
</script>
</body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment