Created
October 10, 2013 23:40
-
-
Save brianmed/6927400 to your computer and use it in GitHub Desktop.
WebRTC DataChannel two user chat. Signaling included using websockets.
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
#!/opt/perl | |
use Mojolicious::Lite; | |
use JSON; | |
get '/' => sub { | |
my $self = shift; | |
$self->render("index"); | |
}; | |
websocket '/wschannel/:type' => sub { | |
my $self = shift; | |
my $type = $self->param("type"); | |
$self->app->log->debug("WebSocket opened: $type"); | |
# Increase inactivity timeout for connection a bit | |
Mojo::IOLoop->stream($self->tx->connection)->timeout(300); | |
my $id = Mojo::IOLoop->recurring(4 => sub { | |
if ("offerer" eq $type) { | |
if (-f "/tmp/sdp.answerer") { | |
$self->app->log->debug("Sending sdp: to offerer from answerer"); | |
my $txt = Mojo::Util::slurp("/tmp/sdp.answerer"); | |
my $ref = JSON::from_json($txt); | |
$self->send({json => $ref}); | |
unlink("/tmp/sdp.answerer"); | |
Mojo::Util::spurt(scalar localtime(time), "/tmp/sdp.offerer.sent"); | |
} | |
foreach my $f (glob("/tmp/candidate.answerer.*")) { | |
$self->app->log->debug("Sending $f: to offerer from answerer"); | |
my $txt = Mojo::Util::slurp($f); | |
my $ref = JSON::from_json($txt); | |
$self->send({json => $ref}); | |
unlink($f); | |
} | |
} | |
else { | |
if (-f "/tmp/sdp.offerer") { | |
my $txt = Mojo::Util::slurp("/tmp/sdp.offerer"); | |
my $ref = JSON::from_json($txt); | |
$self->app->log->debug("Sending sdp: to answerer from offerer"); | |
$self->send({json => $ref}); | |
unlink("/tmp/sdp.offerer"); | |
} | |
if (-f "/tmp/sdp.offerer.sent") { | |
foreach my $f (glob("/tmp/candidate.offerer.*")) { | |
$self->app->log->debug("Sending $f: to answerer from offerer"); | |
my $txt = Mojo::Util::slurp($f); | |
my $ref = JSON::from_json($txt); | |
$self->send({json => $ref}); | |
unlink($f); | |
} | |
unlink("/tmp/sdp.offerer.sent"); | |
} | |
} | |
}); | |
$self->on(message => sub { | |
my ($self, $msg) = @_; | |
$self->app->log->debug("msg: $msg: type: $type"); | |
my $ret = JSON::from_json($msg); | |
if ($$ret{sender} && "offerer" eq $$ret{sender} && $$ret{sdp}) { | |
Mojo::Util::spurt($msg, "/tmp/sdp.offerer"); | |
} | |
if ($$ret{sender} && "answerer" eq $$ret{sender} && $$ret{sdp}) { | |
Mojo::Util::spurt($msg, "/tmp/sdp.answerer"); | |
} | |
if ($$ret{sender} && $$ret{candidate}) { | |
foreach my $suffix (qw(001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 end)) { | |
if ("end" eq $suffix) { | |
$self->app->log->debug("We are OUT of candidate suffixes."); | |
last; | |
} | |
my $f = "/tmp/candidate.$$ret{sender}.$suffix"; | |
if (-f $f) { | |
next; | |
} | |
Mojo::Util::spurt($msg, $f); | |
last; | |
} | |
} | |
}); | |
$self->on(finish => sub { | |
my ($self, $code, $reason) = @_; | |
$self->app->log->debug("WebSocket closed with status $code."); | |
Mojo::IOLoop->remove($id); | |
unlink("/tmp/sdp.$type"); | |
}); | |
}; | |
app->start; | |
__DATA__ | |
@@ index.html.ep | |
<script> | |
var isFirefox = !!navigator.mozGetUserMedia; | |
var connection; | |
function appendDIV(data) { | |
var div = document.createElement('div'); | |
div.innerHTML = data; | |
var chatOutput = document.getElementById('chat-output'); | |
chatOutput.insertBefore(div, chatOutput.firstChild); | |
div.tabIndex = 0; | |
div.focus(); | |
} | |
var iceServers = { | |
iceServers: [{ | |
url: 'stun:23.21.150.121' | |
}] | |
}; | |
var optionalRtpDataChannels = { | |
optional: [{DtlsSrtpKeyAgreement: true}, {RtpDataChannels: true }] | |
}; | |
var mediaConstraints = { | |
optional: [], | |
mandatory: { | |
OfferToReceiveAudio: false, | |
OfferToReceiveVideo: false | |
} | |
}; | |
var offerer, answerer, answererDataChannel, offererDataChannel; | |
function createOffer() { | |
if (isFirefox) { | |
offerer = new mozRTCPeerConnection(iceServers, optionalRtpDataChannels); | |
} | |
else { | |
offerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels); | |
} | |
offererDataChannel = offerer.createDataChannel('RTCDataChannel', { | |
reliable: true | |
}); | |
connection = offererDataChannel; | |
console.log("console: offerer"); | |
setChannelEvents(offererDataChannel, 'offerer'); | |
offerer.onicecandidate = function (event) { | |
console.log("onicecandidate: offerer"); | |
if (event.candidate) sendCandidate(event.candidate); | |
if (!event.candidate) returnSDP(); | |
}; | |
function sendCandidate() { | |
appendDIV("offerer: send candidate"); | |
socket.send({ | |
sender: 'offerer', | |
candidate: event.candidate | |
}); | |
} | |
function returnSDP() { | |
appendDIV("offerer: send sdp"); | |
socket.send({ | |
sender: 'offerer', | |
sdp: offerer.localDescription | |
}); | |
} | |
offerer.createOffer(function (sessionDescription) { | |
offerer.setLocalDescription(sessionDescription); | |
}, onError, mediaConstraints); | |
} | |
function onError(err) { | |
console.log(err); | |
} | |
function createAnswer(offerSDP) { | |
if (isFirefox) { | |
answerer = new mozRTCPeerConnection(iceServers, optionalRtpDataChannels); | |
} | |
else { | |
answerer = new webkitRTCPeerConnection(iceServers, optionalRtpDataChannels); | |
} | |
answererDataChannel = answerer.createDataChannel('RTCDataChannel', { | |
reliable: true | |
}); | |
setChannelEvents(answererDataChannel, 'answerer'); | |
connection = answererDataChannel; | |
console.log("console: answer"); | |
answerer.onicecandidate = function (event) { | |
console.log("onicecandidate: answerer"); | |
if (event.candidate) sendCandidate(); | |
if (!event.candidate) returnSDP(); | |
}; | |
function sendCandidate() { | |
appendDIV("answerer: send candidate"); | |
socket.send({ | |
sender: 'answerer', | |
candidate: event.candidate | |
}); | |
} | |
function returnSDP() { | |
appendDIV("answerer: send sdp"); | |
socket.send({ | |
sender: 'answerer', | |
sdp: answerer.localDescription | |
}); | |
} | |
if (isFirefox) { | |
answerer.setRemoteDescription(new mozRTCSessionDescription(offerSDP), function() { | |
answerer.createAnswer(function (sessionDescription) { | |
answerer.setLocalDescription(sessionDescription); | |
}, onError, mediaConstraints); | |
}); | |
} | |
else { | |
answerer.setRemoteDescription(new RTCSessionDescription(offerSDP), function() { | |
answerer.createAnswer(function (sessionDescription) { | |
answerer.setLocalDescription(sessionDescription); | |
}, onError, mediaConstraints); | |
}, onError); | |
} | |
} | |
function setChannelEvents(channel, channelNameForConsoleOutput) { | |
channel.onerror = onError; | |
channel.onmessage = function (event) { | |
console.log("channel: onmessage"); | |
appendDIV(channelNameForConsoleOutput + 'received a message:' + event.data); | |
}; | |
channel.onopen = function () { | |
console.log("channel: onopen"); | |
document.getElementById('chat-input').disabled = false; | |
}; | |
} | |
function connectSignaler(type) { | |
var url; | |
if ("offerer" == type) { | |
url = "<%= url_for('/wschannel/offerer')->to_abs->scheme('ws') %>"; | |
} | |
else { | |
url = "<%= url_for('/wschannel/answerer')->to_abs->scheme('ws') %>"; | |
} | |
console.log("Connecting: " + url); | |
socket = new WebSocket(url); | |
socket.onopen = function () { | |
appendDIV("Connected: " + url); | |
}; | |
socket.onmessage = function (e) { | |
var data = JSON.parse(e.data); | |
console.log("onmessage: " + data.sender); | |
if (data.sdp) { | |
if (data.sender == 'offerer') { | |
appendDIV("recv: sdp: offerer"); | |
createAnswer(data.sdp); | |
} | |
else { | |
appendDIV("recv: sdp: answerer"); | |
if (isFirefox) { | |
offerer.setRemoteDescription(new mozRTCSessionDescription(data.sdp)); | |
} | |
else { | |
offerer.setRemoteDescription(new RTCSessionDescription(data.sdp)); | |
} | |
} | |
} | |
if ("offerer" === data.sender && data.candidate) { | |
appendDIV("recv: candidate: offerer"); | |
if (isFirefox) { | |
answerer.addIceCandidate(new mozRTCIceCandidate({ | |
sdpMLineIndex: data.candidate.sdpMLineIndex, | |
candidate: data.candidate.candidate | |
})); | |
} | |
else { | |
answerer.addIceCandidate(new RTCIceCandidate({ | |
sdpMLineIndex: data.candidate.sdpMLineIndex, | |
candidate: data.candidate.candidate | |
})); | |
} | |
} | |
if ("answerer" === data.sender && data.candidate) { | |
appendDIV("recv: candidate: answerer"); | |
if (isFirefox) { | |
offerer.addIceCandidate(new mozRTCIceCandidate({ | |
sdpMLineIndex: data.candidate.sdpMLineIndex, | |
candidate: data.candidate.candidate | |
})); | |
} | |
else { | |
offerer.addIceCandidate(new RTCIceCandidate({ | |
sdpMLineIndex: data.candidate.sdpMLineIndex, | |
candidate: data.candidate.candidate | |
})); | |
} | |
} | |
}; | |
socket.push = socket.send; | |
socket.send = function (data) { | |
console.log("send: " + data.sender); | |
socket.push(JSON.stringify(data)); | |
}; | |
} | |
</script> | |
Has worked in Chrome.<br /> | |
Create Offer should be clicked by the "Offerer".<br /> | |
<button id="connect-offerer">Connect as Offerer</button> | |
<button id="connect-answerer">Connect as Answerer</button><br /> | |
<button id="create-offer">Create Offer</button> | |
<input type="text" id="chat-input" style="font-size: 1.2em;" placeholder="chat message" disabled> | |
<div id="chat-output"></div> | |
<script> | |
document.getElementById('connect-offerer').onclick = function () { | |
this.disabled = true; | |
connectSignaler("offerer"); | |
}; | |
document.getElementById('connect-answerer').onclick = function () { | |
this.disabled = true; | |
connectSignaler("answerer"); | |
}; | |
document.getElementById('create-offer').onclick = function () { | |
createOffer(); | |
}; | |
var chatInput = document.getElementById('chat-input'); | |
chatInput.onkeypress = function(e) { | |
if (e.keyCode !== 13 || !this.value) return; | |
appendDIV(this.value); | |
console.log(connection); | |
connection.send(this.value); | |
this.value = ''; | |
this.focus(); | |
}; | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment