Skip to content

Instantly share code, notes, and snippets.

@brianmed
Created October 10, 2013 23:40
Show Gist options
  • Save brianmed/6927400 to your computer and use it in GitHub Desktop.
Save brianmed/6927400 to your computer and use it in GitHub Desktop.
WebRTC DataChannel two user chat. Signaling included using websockets.
#!/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