Created
March 14, 2013 17:48
-
-
Save reklis/5163488 to your computer and use it in GitHub Desktop.
webrtc using http://brightcontext.com
This file contains hidden or 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 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> |
This file contains hidden or 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
/*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; | |
}()); |
This file contains hidden or 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
/*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'); | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.