Created
February 19, 2014 23:27
-
-
Save keroserene/9103784 to your computer and use it in GitHub Desktop.
SCTP multi-datachannel test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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