<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebRTC p2p data</title>
<script>
var RTCPeerConnection = null;
var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox");
webrtcDetectedBrowser = "firefox";
// The RTCPeerConnection object.
RTCPeerConnection = mozRTCPeerConnection;
// The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription;
// The RTCIceCandidate object.
RTCIceCandidate = mozRTCIceCandidate;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
console.log("Attaching media stream");
element.mozSrcObject = stream;
element.play();
};
reattachMediaStream = function(to, from) {
console.log("Reattaching media stream");
to.mozSrcObject = from.mozSrcObject;
to.play();
};
// Fake get{Video,Audio}Tracks
MediaStream.prototype.getVideoTracks = function() {
return [];
};
MediaStream.prototype.getAudioTracks = function() {
return [];
};
} else if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome");
webrtcDetectedBrowser = "chrome";
// The RTCPeerConnection object.
RTCPeerConnection = webkitRTCPeerConnection;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
element.src = webkitURL.createObjectURL(stream);
};
reattachMediaStream = function(to, from) {
to.src = from.src;
};
// The representation of tracks in a stream is changed in M26.
// Unify them for earlier Chrome versions in the coexisting period.
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function() {
return this.videoTracks;
};
webkitMediaStream.prototype.getAudioTracks = function() {
return this.audioTracks;
};
}
// New syntax of getXXXStreams method in M26.
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
webkitRTCPeerConnection.prototype.getLocalStreams = function() {
return this.localStreams;
};
webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
return this.remoteStreams;
};
}
} else {
console.log("Browser does not appear to be WebRTC-capable");
}
</script>
<script src="/index/libs/jquery/jquery.2.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link href="css/bootstrap-responsive.css" rel="stylesheet">
<link href="css/serverless-webrtc-bootstrap.css" rel="stylesheet">
</head>
<body>
<a href="https://github.com/cjb/serverless-webrtc/"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png" alt="Fork me on GitHub"></a>
<div class="span12">
<fieldset class="well">
<p class="head muted">
Serverless WebRTC chat demonstration.
</p>
<div class="text-info" id="chatlog" style="height:350px; overflow:auto;">
</div>
</fieldset>
<form class="form-inline" onSubmit="return sendMessage()" action="">
<input type="text" id="messageTextBox" placeholder="Type your message here">
<button type="submit" id="sendMessageBtn" class="btn">Send message</button>
</form>
<input type="file" id="fileBtn">
</div>
<div class="modal" id="showLocalOffer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
<div class="modal-header">
<h3 id="myModalLabel">Send your local offer to someone else</h3>
</div>
<div class="modal-body">
Here's your "offer" -- it tells someone else how to connect to you. Send the whole thing to them, for example in an instant message or e-mail.
<br/>
<textarea class="input-large" id="localOffer" name="localOffer" rows="10" cols="100"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="offerSentBtn" data-dismiss="modal" aria-hidden="true">Okay, I sent it.</button>
</div>
</div>
<div class="modal" id="showLocalAnswer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
<div class="modal-header">
<h3 id="myModalLabel">Send your local answer to someone else</h3>
</div>
<div class="modal-body">
Here's your "answer" -- it tells someone else how to connect to you. Send the whole thing to them, for example in an instant message or e-mail.
<br/>
<textarea class="input-large" id="localAnswer" name="localAnswer" rows="10" cols="100"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="answerSentBtn" data-dismiss="modal" aria-hidden="true">Okay, I sent it.</button>
</div>
</div>
<div class="modal" id="getRemoteOffer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
<div class="modal-header">
<h3 id="myModalLabel">Paste the "offer" you received</h3>
</div>
<div class="modal-body">
The person who created the room will send you an "offer" string -- paste it here.
<br/>
<textarea class="input-large" id="remoteOffer" name="remoteOffer" rows="10" cols="100"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="offerRecdBtn" data-dismiss="modal" aria-hidden="true">Okay, I pasted it.</button>
</div>
</div>
<div class="modal" id="getRemoteAnswer" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
<div class="modal-header">
<h3 id="myModalLabel">Paste the "answer" you received</h3>
</div>
<div class="modal-body">
Now paste in the "answer" that was sent back to you.
<br/>
<textarea class="input-large" id="remoteAnswer" name="remoteAnswer" rows="10" cols="100"></textarea>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="answerRecdBtn" data-dismiss="modal" aria-hidden="true">Okay, I pasted it.</button>
</div>
</div>
<div class="modal" id="waitForConnection" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" hidden>
<div class="modal-header">
<h3 id="myModalLabel">Waiting for connection</h3>
</div>
<div class="modal-body">
This dialog will disappear when a connection is made.
</div>
<div class="spinner" align="center">
<img src="img/spinner.gif"></img>
</div>
</div>
<div class="modal" id="createOrJoin" data-backdrop="static" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<h3 id="myModalLabel">Create or join a room?</h3>
</div>
<div class="modal-footer">
<button class="btn" id="joinBtn" data-dismiss="modal" aria-hidden="true">Join</button>
<button class="btn btn-primary" id="createBtn" data-dismiss="modal" aria-hidden="true">Create</button>
</div>
</div>
<script>
/* See also:
http://www.html5rocks.com/en/tutorials/webrtc/basics/
https://code.google.com/p/webrtc-samples/source/browse/trunk/apprtc/index.html
https://webrtc-demos.appspot.com/html/pc1.html
*/
var cfg = {"iceServers":[{"url":"stun:23.21.150.121"}]},
con = { 'optional': [{'DtlsSrtpKeyAgreement': true}] };
/* THIS IS ALICE, THE CALLER/SENDER */
var pc1 = new RTCPeerConnection(cfg, con),
dc1 = null, tn1 = null;
// Since the same JS file contains code for both sides of the connection,
// activedc tracks which of the two possible datachannel variables we're using.
var activedc;
var pc1icedone = false;
$('#showLocalOffer').modal('hide');
$('#getRemoteAnswer').modal('hide');
$('#waitForConnection').modal('hide');
$('#createOrJoin').modal('show');
$('#createBtn').click(function() {
$('#showLocalOffer').modal('show');
createLocalOffer();
});
$('#joinBtn').click(function() {
$('#getRemoteOffer').modal('show');
});
$('#offerSentBtn').click(function() {
$('#getRemoteAnswer').modal('show');
});
$('#offerRecdBtn').click(function() {
var offer = $('#remoteOffer').val();
var offerDesc = new RTCSessionDescription(JSON.parse(offer));
console.log("Received remote offer", offerDesc);
writeToChatLog("Received remote offer", "text-success");
handleOfferFromPC1(offerDesc);
$('#showLocalAnswer').modal('show');
});
$('#answerSentBtn').click(function() {
$('#waitForConnection').modal('show');
});
$('#answerRecdBtn').click(function() {
var answer = $('#remoteAnswer').val();
var answerDesc = new RTCSessionDescription(JSON.parse(answer));
handleAnswerFromPC2(answerDesc);
$('#waitForConnection').modal('show');
});
$('#fileBtn').change(function() {
var file = this.files[0];
console.log(file);
sendFile(file);
});
function fileSent(file) {
console.log(file + " sent");
}
function fileProgress(file) {
console.log(file + " progress");
}
function sendFile(data) {
if (data.size) {
FileSender.send({
file: data,
onFileSent: fileSent,
onFileProgress: fileProgress,
});
}
}
function sendMessage() {
if ($('#messageTextBox').val()) {
var channel = new RTCMultiSession();
writeToChatLog($('#messageTextBox').val(), "text-success");
channel.send({message: $('#messageTextBox').val()});
$('#messageTextBox').val("");
// Scroll chat text area to the bottom on new input.
$('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
}
return false;
};
function setupDC1() {
try {
var fileReceiver1 = new FileReceiver();
dc1 = pc1.createDataChannel('test', {reliable:true});
activedc = dc1;
console.log("Created datachannel (pc1)");
dc1.onopen = function (e) {
console.log('data channel connect');
$('#waitForConnection').modal('hide');
$('#waitForConnection').remove();
}
dc1.onmessage = function (e) {
console.log("Got message (pc1)", e.data);
if (e.data.size) {
fileReceiver1.receive(e.data, {});
}
else {
if (e.data.charCodeAt(0) == 2) {
// The first message we get from Firefox (but not Chrome)
// is literal ASCII 2 and I don't understand why -- if we
// leave it in, JSON.parse() will barf.
return;
}
console.log(e);
var data = JSON.parse(e.data);
if (data.type === 'file') {
fileReceiver1.receive(e.data, {});
}
else {
writeToChatLog(data.message, "text-info");
// Scroll chat text area to the bottom on new input.
$('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
}
}
};
} catch (e) { console.warn("No data channel (pc1)", e); }
}
function createLocalOffer() {
setupDC1();
pc1.createOffer(function (desc) {
pc1.setLocalDescription(desc, function () {}, function () {});
console.log("created local offer", desc);
}, function () {console.warn("Couldn't create offer");});
}
pc1.onicecandidate = function (e) {
console.log("ICE candidate (pc1)", e);
if (e.candidate == null) {
$('#localOffer').html(JSON.stringify(pc1.localDescription));
}
};
function handleOnconnection() {
console.log("Datachannel connected");
writeToChatLog("Datachannel connected", "text-success");
$('#waitForConnection').modal('hide');
// If we didn't call remove() here, there would be a race on pc2:
// - first onconnection() hides the dialog, then someone clicks
// on answerSentBtn which shows it, and it stays shown forever.
$('#waitForConnection').remove();
$('#showLocalAnswer').modal('hide');
$('#messageTextBox').focus();
}
pc1.onconnection = handleOnconnection;
function onsignalingstatechange(state) {
console.info('signaling state change:', state);
}
function oniceconnectionstatechange(state) {
console.info('ice connection state change:', state);
}
function onicegatheringstatechange(state) {
console.info('ice gathering state change:', state);
}
pc1.onsignalingstatechange = onsignalingstatechange;
pc1.oniceconnectionstatechange = oniceconnectionstatechange;
pc1.onicegatheringstatechange = onicegatheringstatechange;
function handleAnswerFromPC2(answerDesc) {
console.log("Received remote answer: ", answerDesc);
writeToChatLog("Received remote answer", "text-success");
pc1.setRemoteDescription(answerDesc);
}
function handleCandidateFromPC2(iceCandidate) {
pc1.addIceCandidate(iceCandidate);
}
/* THIS IS BOB, THE ANSWERER/RECEIVER */
var pc2 = new RTCPeerConnection(cfg, con),
dc2 = null;
var pc2icedone = false;
pc2.ondatachannel = function (e) {
var fileReceiver2 = new FileReceiver();
var datachannel = e.channel || e; // Chrome sends event, FF sends raw channel
console.log("Received datachannel (pc2)", arguments);
dc2 = datachannel;
activedc = dc2;
dc2.onopen = function (e) {
console.log('data channel connect');
$('#waitForConnection').modal('hide');
$('#waitForConnection').remove();
}
dc2.onmessage = function (e) {
console.log("Got message (pc2)", e.data);
if (e.data.size) {
fileReceiver2.receive(e.data, {});
}
else {
var data = JSON.parse(e.data);
if (data.type === 'file') {
fileReceiver2.receive(e.data, {});
}
else {
writeToChatLog(data.message, "text-info");
// Scroll chat text area to the bottom on new input.
$('#chatlog').scrollTop($('#chatlog')[0].scrollHeight);
}
}
};
};
function handleOfferFromPC1(offerDesc) {
pc2.setRemoteDescription(offerDesc);
pc2.createAnswer(function (answerDesc) {
writeToChatLog("Created local answer", "text-success");
console.log("Created local answer: ", answerDesc);
pc2.setLocalDescription(answerDesc);
}, function () { console.warn("No create answer"); });
}
pc2.onicecandidate = function (e) {
console.log("ICE candidate (pc2)", e);
if (e.candidate == null)
$('#localAnswer').html(JSON.stringify(pc2.localDescription));
};
pc2.onsignalingstatechange = onsignalingstatechange;
pc2.oniceconnectionstatechange = oniceconnectionstatechange;
pc2.onicegatheringstatechange = onicegatheringstatechange;
function handleCandidateFromPC1(iceCandidate) {
pc2.addIceCandidate(iceCandidate);
}
pc2.onaddstream = function (e) {
console.log("Got remote stream", e);
var el = new Audio();
el.autoplay = true;
attachMediaStream(el, e.stream);
};
pc2.onconnection = handleOnconnection;
function getTimestamp() {
var totalSec = new Date().getTime() / 1000;
var hours = parseInt(totalSec / 3600) % 24;
var minutes = parseInt(totalSec / 60) % 60;
var seconds = parseInt(totalSec % 60);
var result = (hours < 10 ? "0" + hours : hours) + ":" +
(minutes < 10 ? "0" + minutes : minutes) + ":" +
(seconds < 10 ? "0" + seconds : seconds);
return result;
}
function writeToChatLog(message, message_type) {
document.getElementById('chatlog').innerHTML += '<p class=\"' + message_type + '\">' + "[" + getTimestamp() + "] " + message + '</p>';
}
</script>
<script>
/* MIT License: https://webrtc-experiment.appspot.com/licence/
* 2013, Muaz Khan<muazkh>--[ github.com/muaz-khan ]
*/
/* For documentation and examples: http://bit.ly/RTCDataConnection */
window.moz = !! navigator.mozGetUserMedia;
var RTCMultiSession = function(options) {
return {
send: function (message) {
if (moz && message.file)
data = message.file;
else
data = JSON.stringify(message);
activedc.send(data);
}
}
};
var FileSender = {
send: function (config) {
var channel = config.channel || new RTCMultiSession();
var file = config.file;
/* if firefox nightly: share file blob directly */
if (moz) {
/* used on the receiver side to set received file name */
channel.send({
fileName: file.name,
type: 'file'
});
/* sending the entire file at once */
channel.send({
file: file
});
if (config.onFileSent) config.onFileSent(file);
}
/* if chrome */
if (!moz) {
var reader = new window.FileReader();
reader.readAsDataURL(file);
reader.onload = onReadAsDataURL;
}
var packetSize = 1000 /* chars */ ,
textToTransfer = '',
numberOfPackets = 0,
packets = 0;
function onReadAsDataURL(event, text) {
var data = {
type: 'file'
};
if (event) {
text = event.target.result;
numberOfPackets = packets = data.packets = parseInt(text.length / packetSize);
}
if (config.onFileProgress)
config.onFileProgress({
remaining: packets--,
length: numberOfPackets,
sent: numberOfPackets - packets
});
if (text.length > packetSize)
data.message = text.slice(0, packetSize);
else {
data.message = text;
data.last = true;
data.name = file.name;
if (config.onFileSent) config.onFileSent(file);
}
channel.send(data);
textToTransfer = text.slice(data.message.length);
if (textToTransfer.length)
setTimeout(function () {
onReadAsDataURL(null, textToTransfer);
}, 500);
}
}
};
function FileReceiver() {
var content = [],
fileName = '',
packets = 0,
numberOfPackets = 0;
function receive(data, config) {
/* if firefox nightly & file blob shared */
if (moz) {
if (!data.size) {
var parsedData = JSON.parse(data);
if (parsedData.fileName) {
fileName = parsedData.fileName;
}
}
else {
var reader = new window.FileReader();
reader.readAsDataURL(data);
reader.onload = function (event) {
FileSaver.SaveToDisk(event.target.result, fileName);
if (config.onFileReceived) config.onFileReceived(fileName);
};
}
}
if (!moz) {
if (data.packets)
numberOfPackets = packets = parseInt(data.packets);
if (config.onFileProgress)
config.onFileProgress({
remaining: packets--,
length: numberOfPackets,
received: numberOfPackets - packets
});
content.push(data.message);
if (data.last) {
FileSaver.SaveToDisk(content.join(''), data.name);
if (config.onFileReceived)
config.onFileReceived(data.name);
content = [];
}
}
}
return {
receive: receive
};
}
var FileSaver = {
SaveToDisk: function (fileUrl, fileName) {
var save = document.createElement('a');
save.href = fileUrl;
save.target = '_blank';
save.download = fileName || fileUrl;
var evt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
save.dispatchEvent(evt);
(window.URL || window.webkitURL).revokeObjectURL(save.href);
}
};
</script>
</body>
</html>