Skip to content

Instantly share code, notes, and snippets.

@reklis
Created March 14, 2013 17:48
Show Gist options
  • Save reklis/5163488 to your computer and use it in GitHub Desktop.
Save reklis/5163488 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>bcc + webrtc</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.2/css/bootstrap.no-icons.min.css" rel="stylesheet">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.0.2/css/font-awesome.css" rel="stylesheet">
<!--[if lt IE 8]>
<link href="//netdna.bootstrapcdn.com/font-awesome/3.0.2/css/font-awesome-ie7.css" rel="stylesheet">
<![endif]-->
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.6.1/html5shiv.js "></script>
<![endif]-->
<!--[if IE]
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/es5-shim/1.2.4/es5-shim.min.js"></script>
<![endif]-->
<!--
<link rel="shortcut icon" href="ico/favicon.ico">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="ico/apple-touch-icon-144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="ico/apple-touch-icon-114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="ico/apple-touch-icon-72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="ico/apple-touch-icon-57-precomposed.png">
-->
<!--
<meta name="msapplication-TileColor" content="#123456"/>
<meta name="msapplication-TileImage" content="ico/ms-tile-icon.png"/>
-->
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<!--[if IE 6]><body class="ie ie6"><![endif]-->
<!--[if IE 7]><body class="ie ie7"><![endif]-->
<!--[if IE 8]><body class="ie ie8"><![endif]-->
<!--[if IE 9]><body class="ie ie9"><![endif]-->
<!--[if IE 10]><body class="ie ie10"><![endif]-->
<!--[if gt IE 10]><body class="ie"><![endif]-->
<!--[if !IE ]><!--><body><!--<![endif]-->
<div class="container">
<button id="btnHangup" class="btn disabled">Hangup</button>
<div id="card">
<div id="local">
<video width="100%" height="100%" id="localVideo" autoplay="autoplay" muted="true">
</video>
</div>
<div id="remote">
<video width="100%" height="100%" id="remoteVideo" autoplay="autoplay">
</video>
<div id="mini">
<video width="100%" height="100%" id="miniVideo" autoplay="autoplay" muted="true">
</video>
</div>
</div>
</div>
<!-- outgoing call dialog -->
<div id="establishDialog" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="establishDialogLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="establishDialogLabel">Connect</h3>
</div>
<div class="modal-body">
<label for="room_id">Join Room:</label>
<input type="text" name="room_id" id="room_id" placeholder="room id">
<label for="room_id">As User:</label>
<input type="text" name="user_id" id="user_id" placeholder="user id">
</div>
<div class="modal-footer">
<button class="btn btn-primary">Join</button>
</div>
</div>
<!-- incoming call dialog -->
<div id="answerDialog" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="answerDialogLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="answerDialogLabel">Incoming Call...</h3>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button class="btn btn-primary">Answer</button>
</div>
</div>
</div> <!-- /container -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
<script src="http://static.brightcontext.com/js-sdk/bcc.min.js"></script>
<script src="js/rtc.js"></script>
<script src="js/script.js"></script>
</body>
</html>
/*global setTimeout, console, window, navigator, webkitURL, webkitMediaStream, webkitRTCPeerConnection */
(function () {
'use strict';
var rtc, crypto, dtls;
rtc = {
isMoz: ('undefined' !== typeof(navigator.mozGetUserMedia)),
isChrome: ('undefined' !== typeof(navigator.webkitGetUserMedia)),
RTCPeerConnection: window.mozRTCPeerConnection || window.webkitRTCPeerConnection,
RTCSessionDescription: window.mozRTCSessionDescription || window.RTCSessionDescription,
RTCIceCandidate: window.mozRTCIceCandidate || window.RTCIceCandidate,
getUserMediaFn: navigator.mozGetUserMedia || navigator.webkitGetUserMedia,
sdpConstraints: {'mandatory': { 'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }}
};
rtc.getUserMedia = rtc.getUserMediaFn.bind(navigator);
if (rtc.isMoz) {
rtc.attachMediaStream = function(element, stream) {
element.mozSrcObject = stream;
element.play();
};
rtc.reattachMediaStream = function(to, from) {
to.mozSrcObject = from.mozSrcObject;
to.play();
};
window.MediaStream.prototype.getVideoTracks = function() {
return [];
};
window.MediaStream.prototype.getAudioTracks = function() {
return [];
};
} else if (rtc.isChrome) {
rtc.attachMediaStream = function(element, stream) {
element.src = webkitURL.createObjectURL(stream);
};
rtc.reattachMediaStream = function(to, from) {
to.src = from.src;
};
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function() {
return this.videoTracks;
};
webkitMediaStream.prototype.getAudioTracks = function() {
return this.audioTracks;
};
}
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
webkitRTCPeerConnection.prototype.getLocalStreams = function() {
return this.localStreams;
};
webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
return this.remoteStreams;
};
}
}
rtc.media = function (completion) {
try {
var constraints = {
'audio': true,
'video': { "mandatory": {}, "optional": [] }
};
rtc.getUserMedia(
constraints,
function (stream) {
completion(null, stream);
},
function (error) {
completion(error);
}
);
} catch (e) {
completion(e);
}
};
rtc.peer = function (participant, room, handler) {
/*
handler = {
miniVideoElement: function () { return element; },
localVideoElement: function () { return element; },
remoteVideoElement: function () { return element; },
onremotestreamadded: function (room, pc, remoteVideo) {},
onremotestreamremoved: function (room, pc, event) {},
onlocalstreamadded: function (room, pc, localStream) {},
onerror: function (err) {}
}
*/
var config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var constraints = {"optional": [{"DtlsSrtpKeyAgreement": true}]};
if (rtc.isMoz) {
config = {"iceServers":[{"url":"stun:23.21.150.121"}]};
}
try {
var pc = new rtc.RTCPeerConnection(config, constraints);
pc.onicecandidate = function (event) {
console.log('onicecandidate');
var c = event.candidate;
if (c) {
room.send({
participant: participant,
type: 'candidate',
label: c.sdpMLineIndex,
id: c.sdpMid,
candidate: c.candidate
});
}
};
pc.onaddstream = function (event) {
console.log('onaddstream');
var remoteStream, miniVideo, localVideo, remoteVideo;
miniVideo = handler.miniVideoElement();
localVideo = handler.localVideoElement();
remoteVideo = handler.remoteVideoElement();
remoteStream = event.stream;
// rtc.reattachMediaStream(miniVideo, localVideo);
rtc.attachMediaStream(remoteVideo, remoteStream);
var waitForRemoteVideo = function () {
var videoTracks = remoteStream.getVideoTracks();
if (videoTracks.length === 0 || remoteVideo.currentTime > 0) {
if ('function' == typeof(handler.onremotestreamadded)) {
handler.onremotestreamadded(room, pc, remoteVideo);
}
} else {
setTimeout(waitForRemoteVideo, 100);
}
};
waitForRemoteVideo();
};
pc.onremovestream = function (event) {
console.log('onremovestream');
if ('function' == typeof(handler.onremotestreamremoved)) {
handler.onremotestreamremoved(room, pc, event);
}
};
rtc.media(function (media_error, localStream) {
if (media_error) {
handler.onerror(media_error);
} else {
pc.addStream(localStream);
var localVideo = handler.localVideoElement();
rtc.attachMediaStream(localVideo, localStream);
if ('function' == typeof(handler.onlocalstreamadded)) {
handler.onlocalstreamadded(room, pc, localStream);
}
}
});
return pc;
} catch (e) {
handler.onerror(e);
}
};
rtc.offer = function (participant, room, pc) {
var constraints = {"optional": [], "mandatory": {"MozDontOfferDataChannel": true}};
// temporary measure to remove Moz* constraints in Chrome
if (rtc.isChrome) {
for (var prop in constraints.mandatory) {
if (prop.indexOf("Moz") != -1) {
delete constraints.mandatory[prop];
}
}
}
constraints = mergeConstraints(constraints, rtc.sdpConstraints);
pc.createOffer(
function (sessionDescription) {
setLocalAndShareDescription(participant, room, pc, 'offer', sessionDescription);
},
function (error) {
console.error(error);
},
constraints
);
};
rtc.remote = function (pc, description) {
var rsd = new rtc.RTCSessionDescription(description);
pc.setRemoteDescription(rsd);
};
rtc.answer = function (participant, room, pc, offer) {
pc.createAnswer(
function (sessionDescription) {
setLocalAndShareDescription(participant, room, pc, 'answer', sessionDescription);
},
function (error) {
console.error(error);
},
rtc.sdpConstraints
);
};
rtc.candidate = function (participant, pc, msg) {
var candidate = new rtc.RTCIceCandidate({
sdpMLineIndex:msg.label,
candidate:msg.candidate
});
pc.addIceCandidate(candidate);
};
rtc.establish = function (handler) {
/*
handler = {
project: BCC.init().project(),
channel: 'channel name',
room_id: 'unique room identifier string shared by both parties',
participant: 'unique string for the individual user',
mini: 'document selector',
local: 'document selector',
remote: 'document selector',
onremotestreamadded: function (room, pc, remoteVideo) { },
onremotestreamremoved: function (room, pc, event) { },
onlocalstreamadded: function (room, pc, localStream) { },
onerror: function(error) { }
}
*/
var pc, commandcode, findelement, project, channel, room_id, participant, mini, local, remote;
commandcode = function (msg) {
return (msg.participant != participant && pc) ? msg.type : undefined;
};
findelement = function (selector) {
return function() { return document.querySelector(selector); };
};
project = handler.project;
channel = handler.channel;
room_id = handler.room_id;
participant = handler.participant;
mini = handler.mini || '#miniVideo';
local = handler.local || '#localVideo';
remote = handler.remote || '#remoteVideo';
project.feed({
channel: channel,
name: room_id,
onopen: function (room) {
pc = rtc.peer(participant, room, {
miniVideoElement: findelement(mini),
localVideoElement: findelement(local),
remoteVideoElement: findelement(remote),
onremotestreamadded: handler.onremotestreamadded,
onremotestreamremoved: handler.onremotestreamremoved,
onlocalstreamadded: function (room, pc, localStream) {
rtc.hello(participant, room);
if ('function' == typeof(handler.onlocalstreamadded)) {
handler.onlocalstreamadded(room, pc, localStream);
}
},
onerror: handler.onerror
});
},
onmsgreceived: function (room, msg) {
if (msg.participant == participant) {
return;
}
var msgcode = commandcode(msg);
switch (msgcode) {
case 'handshake': {
rtc.offer(participant, room, pc, msg);
}
break;
case 'offer': {
rtc.remote(pc, msg.description);
rtc.answer(participant, room, pc, msg);
}
break;
case 'answer': {
rtc.remote(pc, msg.description);
}
break;
case 'candidate': {
rtc.candidate(participant, pc, msg);
}
break;
case 'hangup': {
rtc.hangup(participant, room, pc);
}
break;
default: break;
}
},
onerror: handler.onerror
});
};
rtc.hello = function (participant, room) {
room.send({ participant: participant, type: 'handshake' });
};
rtc.hangup = function (participant, room, pc) {
pc.onicecandidate = null;
pc.onaddstream = null;
pc.onremovestream = null;
pc.close();
pc = null;
room.send({
participant: participant,
type: 'hangup'
});
};
// ---
function mergeConstraints(cons1, cons2) {
var merged = cons1;
for (var name in cons2.mandatory) {
merged.mandatory[name] = cons2.mandatory[name];
}
merged.optional.concat(cons2.optional);
return merged;
}
function setLocalAndShareDescription(participant, room, pc, type, sessionDescription) {
sessionDescription.sdp = preferOpus(sessionDescription.sdp);
pc.setLocalDescription(sessionDescription);
room.send({
participant: participant,
type: type,
description: sessionDescription
});
}
// Set Opus as the default audio codec if it's present.
function preferOpus(sdp) {
var i, mLineIndex, sdpLines = sdp.split('\r\n');
// Search for m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('m=audio') !== -1) {
mLineIndex = i;
break;
}
}
if (mLineIndex === null)
return sdp;
// If Opus is available, set it as the default in m line.
for (i = 0; i < sdpLines.length; i++) {
if (sdpLines[i].search('opus/48000') !== -1) {
var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
if (opusPayload)
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
break;
}
}
// Remove CN in m line and sdp.
sdpLines = removeCN(sdpLines, mLineIndex);
sdp = sdpLines.join('\r\n');
return sdp;
}
function extractSdp(sdpLine, pattern) {
var result = sdpLine.match(pattern);
return (result && result.length == 2)? result[1]: null;
}
// Set the selected codec to the first in m line.
function setDefaultCodec(mLine, payload) {
var elements = mLine.split(' ');
var newLine = [];
var index = 0;
for (var i = 0; i < elements.length; i++) {
if (index === 3) // Format of media starts from the fourth.
newLine[index++] = payload; // Put target payload to the first.
if (elements[i] !== payload)
newLine[index++] = elements[i];
}
return newLine.join(' ');
}
// Strip CN from sdp before CN constraints is ready.
function removeCN(sdpLines, mLineIndex) {
var mLineElements = sdpLines[mLineIndex].split(' ');
// Scan from end for the convenience of removing an item.
for (var i = sdpLines.length-1; i >= 0; i--) {
var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
if (payload) {
var cnPos = mLineElements.indexOf(payload);
if (cnPos !== -1) {
// Remove CN payload from m line.
mLineElements.splice(cnPos, 1);
}
// Remove CN line in sdp
sdpLines.splice(i, 1);
}
}
sdpLines[mLineIndex] = mLineElements.join(' ');
return sdpLines;
}
window.rtc = rtc;
}());
/*global $, BCC, rtc, console */
(function () {
'use strict';
var establishDialog = $('#establishDialog');
var answerDialog = $('#answerDialog');
var btnJoinRoom = $('#btnJoinRoom');
var btnHangup = $('#btnHangup');
var btnMedia = $('#btnMedia');
BCC.setLogLevel(BCC.LogLevel.DEBUG);
var ctx = BCC.init('my api key');
var project = ctx.project('my project name');
var incoming_call, participant_id;
establishDialog.find('.btn-primary').click(function () {
participant_id = participantIdFromForm();
rtc.establish({
project: project,
channel: 'rtc',
room_id: roomIdFromForm(),
participant: participant_id,
onremotestreamadded: function (room, pc, remoteVideo) {
incoming_call = { room: room, pc: pc };
btnHangup.removeClass('disabled');
},
onremotestreamremoved: function (room, pc, event) {
btnHangup.addClass('disabled');
},
onerror: function(error) {
console.error(error);
}
});
establishDialog.modal('hide');
});
answerDialog.find('.btn-primary').click(function () {
rtc.hello(incoming_call.participant, incoming_call.room);
answerDialog.modal('hide');
});
btnHangup.click(function () {
rtc.hangup(participant_id, incoming_call.room, incoming_call.pc);
});
btnMedia.click(function () {
rtc.media(function (media_error, localStream) {
if (media_error) {
console.error(media_error);
} else {
var localVideo = document.getElementById("localVideo");
rtc.attachMediaStream(localVideo, localStream);
}
});
});
function roomIdFromQueryString () {
return window.location.search.match(/room=(.*)/)[1];
}
function roomIdFromForm () {
return $('#room_id').val();
}
function participantIdFromForm () {
return $('#user_id').val();
}
$('#room_id').val('a');
$('#user_id').val('');
establishDialog.modal('show');
}());
@reklis
Copy link
Author

reklis commented Mar 14, 2013

This example assumes you have a single project with a single ThruChannel named 'rtc'. User's can join a common room, and negotiate to share video and audio directly. Works in Chrome + FF latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment