mirror of https://github.com/jitsi/jitsi-meet
parent
bfd9f2f99c
commit
62530ef123
@ -1,4 +1,5 @@ |
||||
meet |
||||
meet - a colibri.js sample application |
||||
==== |
||||
A WebRTC-powered multi-user videochat. For a live demo, check out either https://meet.estos.de/ or https://meet.jit.si/. |
||||
|
||||
colibri.js sample application |
||||
Built using [colibri.js](https://github.com/ESTOS/colibri.js) and [strophe.jingle](https://github.com/ESTOS/strophe.jingle), powered by the [jitsi-videobridge](https://github.com/jitsi/jitsi-videobridge) and [prosody](http://prosody.im/). |
||||
|
||||
@ -0,0 +1,427 @@ |
||||
/* jshint -W117 */ |
||||
/* application specific logic */ |
||||
var connection = null; |
||||
var focus = null; |
||||
var RTC; |
||||
var RTCPeerConnection = null; |
||||
var nickname = null; |
||||
var sharedKey = ''; |
||||
var roomUrl = null; |
||||
|
||||
function init() { |
||||
RTC = setupRTC(); |
||||
if (RTC === null) { |
||||
window.location.href = '/webrtcrequired.html'; |
||||
return; |
||||
} else if (RTC.browser != 'chrome') { |
||||
window.location.href = '/chromeonly.html'; |
||||
return; |
||||
} |
||||
RTCPeerconnection = RTC.peerconnection; |
||||
|
||||
connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind'); |
||||
/* |
||||
connection.rawInput = function (data) { console.log('RECV: ' + data); }; |
||||
connection.rawOutput = function (data) { console.log('SEND: ' + data); }; |
||||
*/ |
||||
connection.jingle.pc_constraints = RTC.pc_constraints; |
||||
|
||||
var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname; |
||||
|
||||
connection.connect(jid, document.getElementById('password').value, function (status) { |
||||
if (status == Strophe.Status.CONNECTED) { |
||||
console.log('connected'); |
||||
getUserMediaWithConstraints(['audio', 'video'], '360'); |
||||
document.getElementById('connect').disabled = true; |
||||
} else { |
||||
console.log('status', status); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function doJoin() { |
||||
var roomnode = null; |
||||
var path = window.location.pathname; |
||||
var roomjid; |
||||
if (path.length > 1) { |
||||
roomnode = path.substr(1).toLowerCase(); |
||||
} else { |
||||
roomnode = Math.random().toString(36).substr(2, 20); |
||||
window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + roomnode); |
||||
} |
||||
roomjid = roomnode + '@' + config.hosts.muc; |
||||
|
||||
if (config.useNicks) { |
||||
var nick = window.prompt('Your nickname (optional)'); |
||||
if (nick) { |
||||
roomjid += '/' + nick; |
||||
} else { |
||||
roomjid += '/' + Strophe.getNodeFromJid(connection.jid); |
||||
} |
||||
} else { |
||||
roomjid += '/' + Strophe.getNodeFromJid(connection.jid); |
||||
} |
||||
connection.emuc.doJoin(roomjid); |
||||
} |
||||
|
||||
$(document).bind('mediaready.jingle', function (event, stream) { |
||||
connection.jingle.localStream = stream; |
||||
RTC.attachMediaStream($('#localVideo'), stream); |
||||
document.getElementById('localVideo').muted = true; |
||||
document.getElementById('localVideo').autoplay = true; |
||||
document.getElementById('localVideo').volume = 0; |
||||
|
||||
document.getElementById('largeVideo').volume = 0; |
||||
document.getElementById('largeVideo').src = document.getElementById('localVideo').src; |
||||
doJoin(); |
||||
}); |
||||
|
||||
$(document).bind('mediafailure.jingle', function () { |
||||
// FIXME
|
||||
}); |
||||
|
||||
$(document).bind('remotestreamadded.jingle', function (event, data, sid) { |
||||
function waitForRemoteVideo(selector, sid) { |
||||
var sess = connection.jingle.sessions[sid]; |
||||
videoTracks = data.stream.getVideoTracks(); |
||||
if (videoTracks.length === 0 || selector[0].currentTime > 0) { |
||||
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
|
||||
$(document).trigger('callactive.jingle', [selector, sid]); |
||||
console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState); |
||||
} else { |
||||
setTimeout(function () { waitForRemoteVideo(selector, sid); }, 100); |
||||
} |
||||
} |
||||
var sess = connection.jingle.sessions[sid]; |
||||
var vid = document.createElement('video'); |
||||
var id = 'remoteVideo_' + sid + '_' + data.stream.id; |
||||
vid.id = id; |
||||
vid.autoplay = true; |
||||
vid.oncontextmenu = function () { return false; }; |
||||
var remotes = document.getElementById('remoteVideos'); |
||||
remotes.appendChild(vid); |
||||
var sel = $('#' + id); |
||||
sel.hide(); |
||||
RTC.attachMediaStream(sel, data.stream); |
||||
waitForRemoteVideo(sel, sid); |
||||
data.stream.onended = function () { |
||||
console.log('stream ended', this.id); |
||||
var src = $('#' + id).attr('src'); |
||||
$('#' + id).remove(); |
||||
if (src === $('#largeVideo').attr('src')) { |
||||
// this is currently displayed as large
|
||||
// pick the last visible video in the row
|
||||
// if nobody else is left, this picks the local video
|
||||
var pick = $('#remoteVideos :visible:last').get(0); |
||||
// mute if localvideo
|
||||
document.getElementById('largeVideo').volume = pick.volume; |
||||
document.getElementById('largeVideo').src = pick.src; |
||||
} |
||||
resizeThumbnails(); |
||||
}; |
||||
sel.click( |
||||
function () { |
||||
console.log('hover in', $(this).attr('src')); |
||||
var newSrc = $(this).attr('src'); |
||||
if ($('#largeVideo').attr('src') != newSrc) { |
||||
document.getElementById('largeVideo').volume = 1; |
||||
$('#largeVideo').fadeOut(300, function () { |
||||
$(this).attr('src', newSrc); |
||||
$(this).fadeIn(300); |
||||
}); |
||||
} |
||||
} |
||||
); |
||||
}); |
||||
|
||||
$(document).bind('callincoming.jingle', function (event, sid) { |
||||
var sess = connection.jingle.sessions[sid]; |
||||
// TODO: check affiliation and/or role
|
||||
console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); |
||||
sess.sendAnswer(); |
||||
sess.accept(); |
||||
}); |
||||
|
||||
$(document).bind('callactive.jingle', function (event, videoelem, sid) { |
||||
console.log('call active'); |
||||
if (videoelem.attr('id').indexOf('mixedmslabel') == -1) { |
||||
// ignore mixedmslabela0 and v0
|
||||
videoelem.show(); |
||||
resizeThumbnails(); |
||||
|
||||
document.getElementById('largeVideo').volume = 1; |
||||
$('#largeVideo').attr('src', videoelem.attr('src')); |
||||
} |
||||
}); |
||||
|
||||
$(document).bind('callterminated.jingle', function (event, sid, reason) { |
||||
// FIXME
|
||||
}); |
||||
|
||||
|
||||
$(document).bind('joined.muc', function (event, jid, info) { |
||||
console.log('onJoinComplete', info); |
||||
updateRoomUrl(window.location.href); |
||||
if (Object.keys(connection.emuc.members).length < 1) { |
||||
focus = new ColibriFocus(connection, config.hosts.bridge); |
||||
return; |
||||
} |
||||
}); |
||||
|
||||
$(document).bind('entered.muc', function (event, jid, info) { |
||||
console.log('entered', jid, info); |
||||
console.log(focus); |
||||
if (focus !== null) { |
||||
// FIXME: this should prepare the video
|
||||
if (focus.confid === null) { |
||||
console.log('make new conference with', jid); |
||||
focus.makeConference(Object.keys(connection.emuc.members)); |
||||
} else { |
||||
console.log('invite', jid, 'into conference'); |
||||
focus.addNewParticipant(jid); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
$(document).bind('left.muc', function (event, jid) { |
||||
console.log('left', jid); |
||||
connection.jingle.terminateByJid(jid); |
||||
// FIXME: this should actually hide the video already for a nicer UX
|
||||
|
||||
if (Object.keys(connection.emuc.members).length === 0) { |
||||
console.log('everyone left'); |
||||
if (focus !== null) { |
||||
// FIXME: closing the connection is a hack to avoid some
|
||||
// problemswith reinit
|
||||
if (focus.peerconnection !== null) { |
||||
focus.peerconnection.close(); |
||||
} |
||||
focus = new ColibriFocus(connection, config.hosts.bridge); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
function toggleVideo() { |
||||
if (!(connection && connection.jingle.localStream)) return; |
||||
for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) { |
||||
connection.jingle.localStream.getVideoTracks()[idx].enabled = !connection.jingle.localStream.getVideoTracks()[idx].enabled; |
||||
} |
||||
} |
||||
|
||||
function toggleAudio() { |
||||
if (!(connection && connection.jingle.localStream)) return; |
||||
for (var idx = 0; idx < connection.jingle.localStream.getAudioTracks().length; idx++) { |
||||
connection.jingle.localStream.getAudioTracks()[idx].enabled = !connection.jingle.localStream.getAudioTracks()[idx].enabled; |
||||
} |
||||
} |
||||
|
||||
function resizeLarge() { |
||||
var availableHeight = window.innerHeight; |
||||
var chatspaceWidth = $('#chatspace').width(); |
||||
|
||||
var numvids = $('#remoteVideos>video:visible').length; |
||||
if (numvids < 5) |
||||
availableHeight -= 100; // min thumbnail height for up to 4 videos
|
||||
else |
||||
availableHeight -= 50; // min thumbnail height for more than 5 videos
|
||||
|
||||
availableHeight -= 79; // padding + link ontop
|
||||
var availableWidth = window.innerWidth - chatspaceWidth; |
||||
var aspectRatio = 16.0 / 9.0; |
||||
if (availableHeight < availableWidth / aspectRatio) { |
||||
availableWidth = Math.floor(availableHeight * aspectRatio); |
||||
} |
||||
if (availableWidth < 0 || availableHeight < 0) return; |
||||
$('#largeVideo').width(availableWidth); |
||||
$('#largeVideo').height(availableWidth / aspectRatio); |
||||
resizeThumbnails(); |
||||
} |
||||
|
||||
function resizeThumbnails() { |
||||
// Calculate the available height, which is the inner window height minus 39px for the header
|
||||
// minus 4px for the delimiter lines on the top and bottom of the large video,
|
||||
// minus the 36px space inside the remoteVideos container used for highlighting shadow.
|
||||
var availableHeight = window.innerHeight - $('#largeVideo').height() - 79; |
||||
var numvids = $('#remoteVideos>video:visible').length; |
||||
// Remove the 1px borders arround videos.
|
||||
var availableWinWidth = $('#remoteVideos').width() - 2 * numvids; |
||||
var availableWidth = availableWinWidth / numvids; |
||||
var aspectRatio = 16.0 / 9.0; |
||||
var maxHeight = Math.min(160, availableHeight); |
||||
availableHeight = Math.min(maxHeight, availableWidth / aspectRatio); |
||||
if (availableHeight < availableWidth / aspectRatio) { |
||||
availableWidth = Math.floor(availableHeight * aspectRatio); |
||||
} |
||||
// size videos so that while keeping AR and max height, we have a nice fit
|
||||
$('#remoteVideos').height(availableHeight + 36); // add the 2*18px border used for highlighting shadow.
|
||||
$('#remoteVideos>video:visible').width(availableWidth); |
||||
$('#remoteVideos>video:visible').height(availableHeight); |
||||
} |
||||
|
||||
$(document).ready(function () { |
||||
$('#nickinput').keydown(function(event) { |
||||
if (event.keyCode == 13) { |
||||
event.preventDefault(); |
||||
var val = this.value; |
||||
this.value = ''; |
||||
if (!nickname) { |
||||
nickname = val; |
||||
$('#nickname').css({visibility:"hidden"}); |
||||
$('#chatconversation').css({visibility:'visible'}); |
||||
$('#usermsg').css({visibility:'visible'}); |
||||
$('#usermsg').focus(); |
||||
return; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
$('#usermsg').keydown(function(event) { |
||||
if (event.keyCode == 13) { |
||||
event.preventDefault(); |
||||
var message = this.value; |
||||
$('#usermsg').val('').trigger('autosize.resize'); |
||||
this.focus(); |
||||
connection.emuc.sendMessage(message, nickname); |
||||
} |
||||
}); |
||||
|
||||
$('#usermsg').autosize(); |
||||
|
||||
resizeLarge(); |
||||
$(window).resize(function () { |
||||
resizeLarge(); |
||||
}); |
||||
if (!$('#settings').is(':visible')) { |
||||
console.log('init'); |
||||
init(); |
||||
} else { |
||||
loginInfo.onsubmit = function (e) { |
||||
if (e.preventDefault) e.preventDefault(); |
||||
$('#settings').hide(); |
||||
init(); |
||||
}; |
||||
} |
||||
}); |
||||
|
||||
$(window).bind('beforeunload', function () { |
||||
if (connection && connection.connected) { |
||||
// ensure signout
|
||||
$.ajax({ |
||||
type: 'POST', |
||||
url: config.bosh, |
||||
async: false, |
||||
cache: false, |
||||
contentType: 'application/xml', |
||||
data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>", |
||||
success: function (data) { |
||||
console.log('signed out'); |
||||
console.log(data); |
||||
}, |
||||
error: function (XMLHttpRequest, textStatus, errorThrown) { |
||||
console.log('signout error', textStatus + ' (' + errorThrown + ')'); |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
function updateChatConversation(nick, message) |
||||
{ |
||||
var divClassName = ''; |
||||
if (nickname == nick) |
||||
divClassName = "localuser"; |
||||
else |
||||
divClassName = "remoteuser"; |
||||
|
||||
$('#chatconversation').append('<div class="' + divClassName + '"><b>' + nick + ': </b>' + message + '</div>'); |
||||
$('#chatconversation').animate({ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); |
||||
} |
||||
|
||||
function buttonClick(id, classname) { |
||||
$(id).toggleClass(classname); // add the class to the clicked element
|
||||
} |
||||
|
||||
function openLockDialog() { |
||||
if (sharedKey) |
||||
$.prompt("Are you sure you would like to remove your secret key?", |
||||
{ |
||||
title: "Remove secrect key", |
||||
persistent: false, |
||||
buttons: { "Remove": true, "Cancel": false}, |
||||
defaultButton: 1, |
||||
submit: function(e,v,m,f){ |
||||
if(v) |
||||
{ |
||||
sharedKey = ''; |
||||
lockRoom(); |
||||
} |
||||
} |
||||
}); |
||||
else |
||||
$.prompt('<h2>Set a secrect key to lock your room</h2>' + |
||||
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>', |
||||
{ |
||||
persistent: false, |
||||
buttons: { "Save": true , "Cancel": false}, |
||||
defaultButton: 1, |
||||
loaded: function(event) { |
||||
document.getElementById('lockKey').focus(); |
||||
}, |
||||
submit: function(e,v,m,f){ |
||||
if(v) |
||||
{ |
||||
var lockKey = document.getElementById('lockKey'); |
||||
|
||||
if (lockKey.value != null) |
||||
{ |
||||
sharedKey = lockKey.value; |
||||
lockRoom(true); |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function openLinkDialog() { |
||||
$.prompt('<input id="inviteLinkRef" type="text" value="' + roomUrl + '" onclick="this.select();">', |
||||
{ |
||||
title: "Share this link with everyone you want to invite", |
||||
persistent: false, |
||||
buttons: { "Cancel": false}, |
||||
loaded: function(event) { |
||||
document.getElementById('inviteLinkRef').select(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function lockRoom(lock) { |
||||
connection.emuc.lockRoom(sharedKey); |
||||
|
||||
buttonClick("#lockIcon", "fa fa-unlock fa-lg fa fa-lock fa-lg"); |
||||
} |
||||
|
||||
function openChat() { |
||||
var chatspace = $('#chatspace'); |
||||
var videospace = $('#videospace'); |
||||
var chatspaceWidth = chatspace.width(); |
||||
|
||||
if (chatspace.css("opacity") == 1) { |
||||
chatspace.animate({opacity: 0}, "fast"); |
||||
chatspace.animate({width: 0}, "slow"); |
||||
videospace.animate({right: 0, width:"100%"}, "slow"); |
||||
} |
||||
else { |
||||
chatspace.animate({width:"20%"}, "slow"); |
||||
chatspace.animate({opacity: 1}, "slow"); |
||||
videospace.animate({right:chatspaceWidth, width:"80%"}, "slow"); |
||||
} |
||||
|
||||
// Request the focus in the nickname field or the chat input field.
|
||||
if ($('#nickinput').is(':visible')) |
||||
$('#nickinput').focus(); |
||||
else |
||||
$('#usermsg').focus(); |
||||
} |
||||
|
||||
function updateRoomUrl(newRoomUrl) { |
||||
roomUrl = newRoomUrl; |
||||
} |
||||
@ -0,0 +1 @@ |
||||
Sorry, this currently only works with chrome because it uses "Plan B". |
||||
@ -0,0 +1,9 @@ |
||||
var config = { |
||||
hosts: { |
||||
domain: 'your.domain.example', |
||||
muc: 'conference.your.domain.example', // FIXME: use XEP-0030
|
||||
bridge: 'jitsi-videobridge.your.domain.example' // FIXME: use XEP-0030
|
||||
}, |
||||
useNicks: false, |
||||
bosh: '/http-bind' // FIXME: use xep-0156 for that
|
||||
}; |
||||
@ -0,0 +1,125 @@ |
||||
/* |
||||
------------------------------ |
||||
Impromptu |
||||
------------------------------ |
||||
*/ |
||||
.jqifade{ |
||||
position: absolute; |
||||
background-color: #000; |
||||
} |
||||
div.jqi{ |
||||
width: 400px; |
||||
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; |
||||
position: absolute; |
||||
background-color: #ffffff; |
||||
font-size: 11px; |
||||
text-align: left; |
||||
border: solid 1px #eeeeee; |
||||
border-radius: 6px; |
||||
-moz-border-radius: 6px; |
||||
-webkit-border-radius: 6px; |
||||
padding: 7px; |
||||
} |
||||
div.jqi .jqicontainer{ |
||||
} |
||||
div.jqi .jqiclose{ |
||||
position: absolute; |
||||
top: 4px; right: -2px; |
||||
width: 18px; |
||||
cursor: default; |
||||
color: #bbbbbb; |
||||
font-weight: bold; |
||||
} |
||||
div.jqi .jqistate{ |
||||
background-color: #fff; |
||||
} |
||||
div.jqi .jqititle{ |
||||
padding: 5px 10px; |
||||
font-size: 16px; |
||||
line-height: 20px; |
||||
border-bottom: solid 1px #eeeeee; |
||||
} |
||||
div.jqi .jqimessage{ |
||||
padding: 10px; |
||||
line-height: 20px; |
||||
color: #444444; |
||||
} |
||||
div.jqi .jqibuttons{ |
||||
text-align: right; |
||||
margin: 0 -7px -7px -7px; |
||||
border-top: solid 1px #e4e4e4; |
||||
background-color: #f4f4f4; |
||||
border-radius: 0 0 6px 6px; |
||||
-moz-border-radius: 0 0 6px 6px; |
||||
-webkit-border-radius: 0 0 6px 6px; |
||||
} |
||||
div.jqi .jqibuttons button{ |
||||
margin: 0; |
||||
padding: 5px 20px; |
||||
background-color: transparent; |
||||
font-weight: normal; |
||||
border: none; |
||||
border-left: solid 1px #e4e4e4; |
||||
color: #777; |
||||
font-weight: bold; |
||||
font-size: 12px; |
||||
} |
||||
div.jqi .jqibuttons button.jqidefaultbutton{ |
||||
color: #489afe; |
||||
} |
||||
div.jqi .jqibuttons button:hover, |
||||
div.jqi .jqibuttons button:focus{ |
||||
color: #287ade; |
||||
outline: none; |
||||
} |
||||
.jqiwarning .jqi .jqibuttons{ |
||||
background-color: #b95656; |
||||
} |
||||
|
||||
/* sub states */ |
||||
div.jqi .jqiparentstate::after{ |
||||
background-color: #777; |
||||
opacity: 0.6; |
||||
filter: alpha(opacity=60); |
||||
content: ''; |
||||
position: absolute; |
||||
top:0;left:0;bottom:0;right:0; |
||||
border-radius: 6px; |
||||
-moz-border-radius: 6px; |
||||
-webkit-border-radius: 6px; |
||||
} |
||||
div.jqi .jqisubstate{ |
||||
position: absolute; |
||||
top:0; |
||||
left: 20%; |
||||
width: 60%; |
||||
padding: 7px; |
||||
border: solid 1px #eeeeee; |
||||
border-top: none; |
||||
border-radius: 0 0 6px 6px; |
||||
-moz-border-radius: 0 0 6px 6px; |
||||
-webkit-border-radius: 0 0 6px 6px; |
||||
} |
||||
div.jqi .jqisubstate .jqibuttons button{ |
||||
padding: 10px 18px; |
||||
} |
||||
|
||||
/* arrows for tooltips/tours */ |
||||
.jqi .jqiarrow{ position: absolute; height: 0; width:0; line-height: 0; font-size: 0; border: solid 10px transparent;} |
||||
|
||||
.jqi .jqiarrowtl{ left: 10px; top: -20px; border-bottom-color: #ffffff; } |
||||
.jqi .jqiarrowtc{ left: 50%; top: -20px; border-bottom-color: #ffffff; margin-left: -10px; } |
||||
.jqi .jqiarrowtr{ right: 10px; top: -20px; border-bottom-color: #ffffff; } |
||||
|
||||
.jqi .jqiarrowbl{ left: 10px; bottom: -20px; border-top-color: #ffffff; } |
||||
.jqi .jqiarrowbc{ left: 50%; bottom: -20px; border-top-color: #ffffff; margin-left: -10px; } |
||||
.jqi .jqiarrowbr{ right: 10px; bottom: -20px; border-top-color: #ffffff; } |
||||
|
||||
.jqi .jqiarrowlt{ left: -20px; top: 10px; border-right-color: #ffffff; } |
||||
.jqi .jqiarrowlm{ left: -20px; top: 50%; border-right-color: #ffffff; margin-top: -10px; } |
||||
.jqi .jqiarrowlb{ left: -20px; bottom: 10px; border-right-color: #ffffff; } |
||||
|
||||
.jqi .jqiarrowrt{ right: -20px; top: 10px; border-left-color: #ffffff; } |
||||
.jqi .jqiarrowrm{ right: -20px; top: 50%; border-left-color: #ffffff; margin-top: -10px; } |
||||
.jqi .jqiarrowrb{ right: -20px; bottom: 10px; border-left-color: #ffffff; } |
||||
|
||||
@ -0,0 +1,318 @@ |
||||
html, body{ |
||||
margin:0px; |
||||
height:100%; |
||||
color: #424242; |
||||
font-family:'YanoneKaffeesatzLight',Verdana,Tahoma,Arial; |
||||
font-weight: 400; |
||||
background: #e9e9e9; |
||||
} |
||||
|
||||
|
||||
#videospace { |
||||
display: block; |
||||
position: absolute; |
||||
top: 39px; |
||||
left: 0px; |
||||
right: 0px; |
||||
float: left; |
||||
} |
||||
|
||||
#largeVideo { |
||||
display:block; |
||||
position:relative; |
||||
width:1280px; |
||||
height:720px; |
||||
margin-left:auto; |
||||
margin-right:auto; |
||||
z-index: 0; |
||||
} |
||||
|
||||
#remoteVideos { |
||||
display:block; |
||||
position:relative; |
||||
text-align:center; |
||||
height:196px; |
||||
width:auto; |
||||
overflow: hidden; |
||||
border:1px solid transparent; |
||||
font-size:0; |
||||
z-index: 2; |
||||
} |
||||
|
||||
#remoteVideos video { |
||||
position:relative; |
||||
top:18px; |
||||
height:160px; |
||||
width:auto; |
||||
z-index:0; |
||||
border:1px solid #FFFFFF; |
||||
} |
||||
|
||||
#remoteVideos video:hover { |
||||
cursor: pointer; |
||||
cursor: hand; |
||||
transform:scale(1.08, 1.08); |
||||
-webkit-transform:scale(1.08, 1.08); |
||||
transition-duration: 0.5s; |
||||
-webkit-transition-duration: 0.5s; |
||||
background-color: #FFFFFF; |
||||
-webkit-animation-name: greyPulse; |
||||
-webkit-animation-duration: 2s; |
||||
-webkit-animation-iteration-count: 1; |
||||
-webkit-box-shadow: 0 0 18px #515151; |
||||
border:1px solid #FFFFFF; |
||||
z-index: 10; |
||||
} |
||||
|
||||
#chatspace { |
||||
display:block; |
||||
position:absolute; |
||||
float: right; |
||||
top: 40px; |
||||
bottom: 0px; |
||||
right: 0px; |
||||
width:0; |
||||
opacity: 0; |
||||
overflow: hidden; |
||||
background-color:#f6f6f6; |
||||
border-left:1px solid #424242; |
||||
} |
||||
|
||||
#chatconversation { |
||||
display:block; |
||||
position:relative; |
||||
top: -120px; |
||||
float:top; |
||||
text-align:left; |
||||
line-height:20px; |
||||
font-size:14px; |
||||
padding:5px; |
||||
height:90%; |
||||
overflow:scroll; |
||||
visibility:hidden; |
||||
} |
||||
|
||||
div.localuser { |
||||
color: #087dba; |
||||
} |
||||
|
||||
div.remoteuser { |
||||
color: #424242; |
||||
} |
||||
|
||||
#usermsg { |
||||
position:absolute; |
||||
bottom: 5px; |
||||
left: 5px; |
||||
right: 5px; |
||||
width: 95%; |
||||
height: 40px; |
||||
z-index: 5; |
||||
visibility:hidden; |
||||
max-height:150px; |
||||
} |
||||
|
||||
#nickname { |
||||
position:relative; |
||||
text-align:center; |
||||
color: #9d9d9d; |
||||
font-size: 18; |
||||
top: 100px; |
||||
left: 5px; |
||||
right: 5px; |
||||
width: 95%; |
||||
} |
||||
|
||||
#nickinput { |
||||
margin-top: 20px; |
||||
font-size: 14; |
||||
} |
||||
|
||||
div#spacer { |
||||
height:5px; |
||||
} |
||||
|
||||
#settings { |
||||
display:none; |
||||
} |
||||
|
||||
#nowebrtc { |
||||
display:none; |
||||
} |
||||
|
||||
div#header{ |
||||
display:block; |
||||
position:relative; |
||||
width:100%; |
||||
height:39px; |
||||
z-index: 1; |
||||
text-align:center; |
||||
background-color:#087dba; |
||||
} |
||||
|
||||
div#left { |
||||
display:block; |
||||
position: absolute; |
||||
left: 0px; |
||||
top: 0px; |
||||
width: 100px; |
||||
height: 39px; |
||||
background-image:url(../images/left1.png); |
||||
background-repeat:no-repeat; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
div#leftlogo { |
||||
position:absolute; |
||||
top: 5px; |
||||
left: 15px; |
||||
background-image:url(../images/jitsilogo.png); |
||||
background-repeat:no-repeat; |
||||
height: 31px; |
||||
width: 68px; |
||||
z-index:1; |
||||
} |
||||
|
||||
div#link { |
||||
display:block; |
||||
position:relative; |
||||
height:39px; |
||||
width:auto; |
||||
overflow: hidden; |
||||
z-index:0; |
||||
} |
||||
|
||||
.button { |
||||
display: inline-block; |
||||
position: relative; |
||||
color: #FFFFFF; |
||||
top: 0; |
||||
padding: 10px 0px; |
||||
height: 19px; |
||||
width: 39px; |
||||
cursor: pointer; |
||||
font-size: 19px; |
||||
text-align: center; |
||||
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); |
||||
} |
||||
|
||||
a.button:hover { |
||||
top: 0; |
||||
cursor: pointer; |
||||
background: rgba(0, 0, 0, 0.3); |
||||
border-radius: 5px; |
||||
background-clip: padding-box; |
||||
-webkit-border-radius: 5px; |
||||
-webkit-background-clip: padding-box; |
||||
} |
||||
|
||||
.no-fa-video-camera, .fa-microphone-slash { |
||||
color: #636363; |
||||
} |
||||
|
||||
.fade_line { |
||||
height: 1px; |
||||
background: black; |
||||
background: -webkit-gradient(linear, 0 0, 100% 0, from(#e9e9e9), to(#e9e9e9), color-stop(50%, black)); |
||||
} |
||||
|
||||
.header_button_separator { |
||||
display: inline-block; |
||||
position:relative; |
||||
top: 7; |
||||
width: 1px; |
||||
height: 25px; |
||||
background: white; |
||||
background: -webkit-gradient(linear, 0 0, 0 100%, from(#087dba), to(#087dba), color-stop(50%, white)); |
||||
} |
||||
|
||||
div#right { |
||||
display:block; |
||||
position:absolute; |
||||
right: 0px; |
||||
top: 0px; |
||||
background-image:url(../images/right1.png); |
||||
background-repeat:no-repeat; |
||||
margin:0; |
||||
padding:0; |
||||
width:100px; |
||||
height:39px; |
||||
} |
||||
div#rightlogo { |
||||
position:absolute; |
||||
top: 6px; |
||||
right: 15px; |
||||
background-image:url(../images/estoslogo.png); |
||||
background-repeat:no-repeat; |
||||
height: 25px; |
||||
width: 62px; |
||||
z-index:1; |
||||
} |
||||
|
||||
input, textarea { |
||||
border: 0px none; |
||||
display: inline-block; |
||||
font-size: 14px; |
||||
padding: 5px; |
||||
background: #f3f3f3; |
||||
border-radius: 3px; |
||||
font-weight: 100; |
||||
line-height: 20px; |
||||
height: 40px; |
||||
color: #333; |
||||
font-weight: bold; |
||||
text-align: left; |
||||
border:1px solid #ACD8F0; |
||||
outline: none; /* removes the default outline */ |
||||
resize: none; /* prevents the user-resizing, adjust to taste */ |
||||
} |
||||
|
||||
input, textarea:focus { |
||||
box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able |
||||
replacement to the outline */ |
||||
} |
||||
|
||||
textarea { |
||||
overflow: hidden; |
||||
word-wrap: break-word; |
||||
resize: horizontal; |
||||
} |
||||
|
||||
button.no-icon { |
||||
padding: 0 1em; |
||||
} |
||||
|
||||
button { |
||||
border: none; |
||||
height: 35px; |
||||
padding: 0 1em 0 2em; |
||||
position: relative; |
||||
border-radius: 3px; |
||||
font-weight: bold; |
||||
color: #fff; |
||||
line-height: 35px; |
||||
background: #2c8ad2; |
||||
} |
||||
|
||||
button, input, select, textarea { |
||||
font-size: 100%; |
||||
margin: 0; |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
button, input[type="button"], input[type="reset"], input[type="submit"] { |
||||
cursor: pointer; |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
form { |
||||
display: block; |
||||
} |
||||
|
||||
/* Animated text area. */ |
||||
.animated { |
||||
-webkit-transition: height 0.2s; |
||||
-moz-transition: height 0.2s; |
||||
transition: height 0.2s; |
||||
} |
||||
@ -0,0 +1,22 @@ |
||||
.jqistates h2 { |
||||
padding-bottom: 10px; |
||||
border-bottom: 1px solid #eee; |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
text-align: center; |
||||
color: #424242; |
||||
} |
||||
|
||||
.jqistates input { |
||||
width: 100%; |
||||
margin: 20px 0; |
||||
} |
||||
|
||||
.jqibuttons button { |
||||
margin-right: 5px; |
||||
float:right; |
||||
} |
||||
|
||||
button.jqidefaultbutton #inviteLinkRef { |
||||
color: #2c8ad2; |
||||
} |
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@ -0,0 +1,68 @@ |
||||
<html> |
||||
<head> |
||||
<title>WebRTC, meet the Jitsi Videobridge</title> |
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> |
||||
<script src="libs/strophejingle.bundle.js"></script><!-- strophe.jingle bundle --> |
||||
<script src="libs/colibri.js"></script><!-- colibri focus implementation --> |
||||
<script src="muc.js"></script><!-- simple MUC library --> |
||||
<script src="app.js"></script><!-- application logic --> |
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet"> |
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css" /> |
||||
<link rel="stylesheet" href="css/jquery-impromptu.css"> |
||||
<link rel="stylesheet" href="css/modaldialog.css"> |
||||
<script src="libs/jquery-impromptu.js"></script> |
||||
<script src="libs/jquery.autosize.js"></script> |
||||
<script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path --> |
||||
</head> |
||||
<body> |
||||
<div id="header"> |
||||
<a href="http://jitsi.org" target="_blank"><div id="leftlogo"></div></a> |
||||
<a href="http://www.estos.com/" target="_blank"><div id="rightlogo"></div></a> |
||||
<div id="link"> |
||||
<a class="button" onclick='buttonClick("#mute", "fa fa-microphone fa-lg fa fa-microphone-slash fa-lg");toggleAudio();'><i id="mute" title="Mute / unmute" class="fa fa-microphone fa-lg"></i></a> |
||||
<div class="header_button_separator"></div> |
||||
<a class="button" onclick='buttonClick("#video", "fa fa-video-camera fa-lg fa fa-video-camera no-fa-video-camera fa-lg");toggleVideo();'><i id="video" title="Start / stop camera" class="fa fa-video-camera fa-lg"></i></a> |
||||
<div class="header_button_separator"></div> |
||||
<a class="button" onclick="openLockDialog();"><i id="lockIcon" title="Lock/unlock room" class="fa fa-unlock fa-lg"></i></a> |
||||
<div class="header_button_separator"></div> |
||||
<a class="button" onclick="openLinkDialog();"><i title="Invite others" class="fa fa-link fa-lg"></i></a> |
||||
<div class="header_button_separator"></div> |
||||
<a class="button" onclick='openChat();'><i id="chat" title="Open chat" class="fa fa-comments fa-lg"></i></a> |
||||
<!--i class='fa fa-external-link'> </i>Others can join you by just going to <span id='roomurl'></span--> |
||||
</div> |
||||
</div> |
||||
<div id="settings"> |
||||
<h1>Connection Settings</h1> |
||||
<form id="loginInfo"> |
||||
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label> |
||||
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label> |
||||
<label>BOSH URL: <input id="boshURL" type="text" name="boshURL" placeholder="/http-bind"/></label> |
||||
<input id="connect" type="submit" value="Connect" /> |
||||
</form> |
||||
</div> |
||||
|
||||
<div id="videospace"> |
||||
<div class="fade_line"></div> |
||||
<video id="largeVideo" autoplay oncontextmenu="return false;"></video> |
||||
<div class="fade_line"></div> |
||||
<div id="remoteVideos"> |
||||
<video id="localVideo" autoplay oncontextmenu="return false;" muted/> |
||||
</div> |
||||
</div> |
||||
<div id="chatspace"> |
||||
<div id="nickname"> |
||||
Enter a nickname in the box below |
||||
<form> |
||||
<input type='text' id="nickinput" placeholder='Choose a nickname' autofocus> |
||||
</form> |
||||
</div> |
||||
|
||||
<!--div><i class="fa fa-comments"> </i><span class='nick'></span>: <span class='chattext'></span></div--> |
||||
<div id="chatconversation"></div> |
||||
<textarea id="usermsg" class= "animated" placeholder='Enter text...' autofocus></textarea> |
||||
</div> |
||||
|
||||
<script> |
||||
</script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,814 @@ |
||||
/* colibri.js -- a COLIBRI focus |
||||
* The colibri spec has been submitted to the XMPP Standards Foundation |
||||
* for publications as a XMPP extensions: |
||||
* http://xmpp.org/extensions/inbox/colibri.html
|
||||
* |
||||
* colibri.js is a participating focus, i.e. the focus participates |
||||
* in the conference. The conference itself can be ad-hoc, through a |
||||
* MUC, through PubSub, etc. |
||||
* |
||||
* colibri.js relies heavily on the strophe.jingle library available
|
||||
* from https://github.com/ESTOS/strophe.jingle
|
||||
* and interoperates with the Jitsi videobridge available from |
||||
* https://jitsi.org/Projects/JitsiVideobridge
|
||||
*/ |
||||
/* |
||||
Copyright (c) 2013 ESTOS GmbH |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
||||
*/ |
||||
/* jshint -W117 */ |
||||
function ColibriFocus(connection, bridgejid) { |
||||
this.connection = connection; |
||||
this.bridgejid = bridgejid; |
||||
this.peers = []; |
||||
this.confid = null; |
||||
|
||||
this.peerconnection = null; |
||||
|
||||
this.sid = Math.random().toString(36).substr(2, 12); |
||||
this.connection.jingle.sessions[this.sid] = this; |
||||
this.mychannel = []; |
||||
this.channels = []; |
||||
this.remotessrc = {}; |
||||
|
||||
// ssrc lines to be added on next update
|
||||
this.addssrc = []; |
||||
// ssrc lines to be removed on next update
|
||||
this.removessrc = []; |
||||
|
||||
// silly wait flag
|
||||
this.wait = true; |
||||
} |
||||
|
||||
// creates a conferences with an initial set of peers
|
||||
ColibriFocus.prototype.makeConference = function (peers) { |
||||
var ob = this; |
||||
if (this.confid !== null) { |
||||
console.error('makeConference called twice? Ignoring...'); |
||||
// FIXME: just invite peers?
|
||||
return; |
||||
} |
||||
this.confid = 0; // !null
|
||||
this.peers = []; |
||||
peers.forEach(function (peer) { |
||||
ob.peers.push(peer); |
||||
ob.channels.push([]); |
||||
}); |
||||
|
||||
this.peerconnection = new RTC.peerconnection(this.connection.jingle.ice_config, this.connection.jingle.pc_constraints); |
||||
this.peerconnection.addStream(this.connection.jingle.localStream); |
||||
this.peerconnection.oniceconnectionstatechange = function (event) { |
||||
console.warn('ice connection state changed to', ob.peerconnection.iceConnectionState); |
||||
/* |
||||
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') { |
||||
console.log('adding new remote SSRCs from iceconnectionstatechange'); |
||||
window.setTimeout(function() { ob.modifySources(); }, 1000); |
||||
} |
||||
*/ |
||||
}; |
||||
this.peerconnection.onsignalingstatechange = function (event) { |
||||
console.warn(ob.peerconnection.signalingState); |
||||
/* |
||||
if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') { |
||||
console.log('adding new remote SSRCs from signalingstatechange'); |
||||
window.setTimeout(function() { ob.modifySources(); }, 1000); |
||||
} |
||||
*/ |
||||
}; |
||||
this.peerconnection.onaddstream = function (event) { |
||||
ob.remoteStream = event.stream; |
||||
$(document).trigger('remotestreamadded.jingle', [event, ob.sid]); |
||||
}; |
||||
this.peerconnection.onicecandidate = function (event) { |
||||
ob.sendIceCandidate(event.candidate); |
||||
}; |
||||
this.peerconnection.createOffer( |
||||
function (offer) { |
||||
ob.peerconnection.setLocalDescription( |
||||
offer, |
||||
function () { |
||||
// success
|
||||
// FIXME: could call _makeConference here and trickle candidates later
|
||||
}, |
||||
function (error) { |
||||
console.log('setLocalDescription failed', error); |
||||
} |
||||
); |
||||
}, |
||||
function (error) { |
||||
console.warn(error); |
||||
} |
||||
); |
||||
this.peerconnection.onicecandidate = function (event) { |
||||
console.log('candidate', event.candidate); |
||||
if (!event.candidate) { |
||||
console.log('end of candidates'); |
||||
ob._makeConference(); |
||||
return; |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
ColibriFocus.prototype._makeConference = function () { |
||||
var ob = this; |
||||
var elem = $iq({to: this.bridgejid, type: 'get'}); |
||||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'}); |
||||
|
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp); |
||||
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid); |
||||
localSDP.media.forEach(function (media, channel) { |
||||
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:')); |
||||
elem.c('content', {name: name}); |
||||
elem.c('channel', {initiator: 'false', expire: '15'}); |
||||
|
||||
// FIXME: should reuse code from .toJingle
|
||||
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]); |
||||
for (var j = 0; j < mline.fmt.length; j++) { |
||||
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]); |
||||
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap)); |
||||
elem.up(); |
||||
} |
||||
|
||||
// FIXME: should reuse code from .toJingle
|
||||
elem.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'}); |
||||
var tmp = SDPUtil.iceparams(media, localSDP.session); |
||||
if (tmp) { |
||||
elem.attrs(tmp); |
||||
var fingerprints = SDPUtil.find_lines(media, 'a=fingerprint:', localSDP.session); |
||||
fingerprints.forEach(function (line) { |
||||
tmp = SDPUtil.parse_fingerprint(line); |
||||
//tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
|
||||
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; |
||||
elem.c('fingerprint').t(tmp.fingerprint); |
||||
delete tmp.fingerprint; |
||||
line = SDPUtil.find_line(media, 'a=setup:', ob.session); |
||||
if (line) { |
||||
tmp.setup = line.substr(8); |
||||
} |
||||
elem.attrs(tmp); |
||||
elem.up(); |
||||
}); |
||||
// XEP-0176
|
||||
if (SDPUtil.find_line(media, 'a=candidate:', localSDP.session)) { // add any a=candidate lines
|
||||
lines = SDPUtil.find_lines(media, 'a=candidate:', localSDP.session); |
||||
for (j = 0; j < lines.length; j++) { |
||||
tmp = SDPUtil.candidateToJingle(lines[j]); |
||||
elem.c('candidate', tmp).up(); |
||||
} |
||||
} |
||||
elem.up(); // end of transport
|
||||
} |
||||
elem.up(); // end of channel
|
||||
for (j = 0; j < ob.peers.length; j++) { |
||||
elem.c('channel', {initiator: 'true', expire:'15' }).up(); |
||||
} |
||||
elem.up(); // end of content
|
||||
}); |
||||
|
||||
this.connection.sendIQ(elem, |
||||
function (result) { |
||||
ob.createdConference(result); |
||||
}, |
||||
function (error) { |
||||
console.warn(error); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
// callback when a conference was created
|
||||
ColibriFocus.prototype.createdConference = function (result) { |
||||
console.log('created a conference on the bridge'); |
||||
var tmp; |
||||
|
||||
this.confid = $(result).find('>conference').attr('id'); |
||||
var remotecontents = $(result).find('>conference>content').get(); |
||||
for (var i = 0; i < remotecontents.length; i++) { |
||||
tmp = $(remotecontents[i]).find('>channel').get(); |
||||
this.mychannel.push($(tmp.shift())); |
||||
for (j = 0; j < tmp.length; j++) { |
||||
if (this.channels[j] === undefined) { |
||||
this.channels[j] = []; |
||||
} |
||||
this.channels[j].push(tmp[j]); |
||||
} |
||||
} |
||||
console.log('remote channels', this.channels); |
||||
|
||||
// establish our channel with the bridge
|
||||
// static answer taken from chrome M31, should be replaced by a
|
||||
// dynamic one that is based on our offer FIXME
|
||||
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n'); |
||||
// get the mixed ssrc
|
||||
for (var channel = 0; channel < remotecontents.length; channel++) { |
||||
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); |
||||
// FIXME: check rtp-level-relay-type
|
||||
if (tmp.length) { |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n'; |
||||
} else { |
||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n'; |
||||
} |
||||
|
||||
// FIXME: should take code from .fromJingle
|
||||
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]'); |
||||
if (tmp.length) { |
||||
bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n'; |
||||
bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n'; |
||||
tmp.find('>candidate').each(function () { |
||||
bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this); |
||||
}); |
||||
tmp = tmp.find('>fingerprint'); |
||||
if (tmp.length) { |
||||
bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n'; |
||||
if (tmp.attr('setup')) { |
||||
bridgeSDP.media[channel] += 'a=setup:' + tmp.attr('setup') + '\r\n'; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join(''); |
||||
|
||||
var ob = this; |
||||
this.peerconnection.setRemoteDescription( |
||||
new RTCSessionDescription({type: 'answer', sdp: bridgeSDP.raw}), |
||||
function () { |
||||
console.log('setRemoteDescription success'); |
||||
// remote channels == remotecontents length - 1!
|
||||
for (var i = 0; i < remotecontents.length - 1; i++) { |
||||
ob.initiate(ob.peers[i], true); |
||||
} |
||||
}, |
||||
function (error) { |
||||
console.log('setRemoteDescription failed'); |
||||
} |
||||
); |
||||
|
||||
}; |
||||
|
||||
// send a session-initiate to a new participant
|
||||
ColibriFocus.prototype.initiate = function (peer, isInitiator) { |
||||
var participant = this.peers.indexOf(peer); |
||||
console.log('tell', peer, participant); |
||||
var sdp; |
||||
if (this.peerconnection != null && this.peerconnection.signalingState == 'stable') { |
||||
sdp = new SDP(this.peerconnection.remoteDescription.sdp); |
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp); |
||||
// throw away stuff we don't want
|
||||
// not needed with static offer
|
||||
sdp.removeSessionLines('a=group:'); |
||||
sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
|
||||
for (var i = 0; i < sdp.media.length; i++) { |
||||
sdp.removeMediaLines(i, 'a=rtcp-mux'); |
||||
sdp.removeMediaLines(i, 'a=ssrc:'); |
||||
sdp.removeMediaLines(i, 'a=crypto:'); |
||||
sdp.removeMediaLines(i, 'a=candidate:'); |
||||
sdp.removeMediaLines(i, 'a=ice-options:google-ice'); |
||||
sdp.removeMediaLines(i, 'a=ice-ufrag:'); |
||||
sdp.removeMediaLines(i, 'a=ice-pwd:'); |
||||
sdp.removeMediaLines(i, 'a=fingerprint:'); |
||||
sdp.removeMediaLines(i, 'a=setup:'); |
||||
|
||||
// re-add all remote a=ssrcs
|
||||
for (var jid in this.remotessrc) { |
||||
if (jid == peer) continue; |
||||
sdp.media[i] += this.remotessrc[jid][i]; |
||||
} |
||||
// and local a=ssrc lines
|
||||
sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n'; |
||||
} |
||||
sdp.raw = sdp.session + sdp.media.join(''); |
||||
} else { |
||||
console.error('can not initiate a new session without a stable peerconnection'); |
||||
return; |
||||
} |
||||
|
||||
// add stuff we got from the bridge
|
||||
for (var j = 0; j < sdp.media.length; j++) { |
||||
var chan = $(this.channels[participant][j]); |
||||
console.log('channel id', chan.attr('id')); |
||||
|
||||
tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); |
||||
if (tmp.length) { |
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n'; |
||||
} else { |
||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n'; |
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n'; |
||||
} |
||||
|
||||
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]'); |
||||
if (tmp.length) { |
||||
if (tmp.attr('ufrag')) |
||||
sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n'; |
||||
if (tmp.attr('pwd')) |
||||
sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n'; |
||||
// and the candidates...
|
||||
tmp.find('>candidate').each(function () { |
||||
sdp.media[j] += SDPUtil.candidateFromJingle(this); |
||||
}); |
||||
tmp = tmp.find('>fingerprint'); |
||||
if (tmp.length) { |
||||
sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n'; |
||||
/* |
||||
if (tmp.attr('direction')) { |
||||
sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n'; |
||||
} |
||||
*/ |
||||
sdp.media[j] += 'a=setup:actpass\r\n'; |
||||
} |
||||
} |
||||
} |
||||
// make a new colibri session and configure it
|
||||
// FIXME: is it correct to use this.connection.jid when used in a MUC?
|
||||
var sess = new ColibriSession(this.connection.jid, |
||||
Math.random().toString(36).substr(2, 12), // random string
|
||||
this.connection); |
||||
sess.initiate(peer); |
||||
sess.colibri = this; |
||||
sess.localStream = this.connection.jingle.localStream; |
||||
sess.media_constraints = this.connection.jingle.media_constraints; |
||||
sess.pc_constraints = this.connection.jingle.pc_constraints; |
||||
sess.ice_config = this.connection.ice_config; |
||||
|
||||
this.connection.jingle.sessions[sess.sid] = sess; |
||||
this.connection.jingle.jid2session[sess.peerjid] = sess; |
||||
|
||||
// send a session-initiate
|
||||
var init = $iq({to: peer, type: 'set'}) |
||||
.c('jingle', |
||||
{xmlns: 'urn:xmpp:jingle:1', |
||||
action: 'session-initiate', |
||||
initiator: sess.me, |
||||
sid: sess.sid |
||||
} |
||||
); |
||||
sdp.toJingle(init, 'initiator'); |
||||
this.connection.sendIQ(init, |
||||
function (res) { |
||||
console.log('got result'); |
||||
}, |
||||
function (err) { |
||||
console.log('got error'); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
// pull in a new participant into the conference
|
||||
ColibriFocus.prototype.addNewParticipant = function (peer) { |
||||
var ob = this; |
||||
if (this.confid === 0) { |
||||
// bad state
|
||||
console.log('confid does not exist yet, postponing', peer); |
||||
window.setTimeout(function () { |
||||
ob.addNewParticipant(peer); |
||||
}, 250); |
||||
return; |
||||
} |
||||
var index = this.channels.length; |
||||
this.channels.push([]); |
||||
this.peers.push(peer); |
||||
|
||||
var elem = $iq({to: this.bridgejid, type: 'get'}); |
||||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); |
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp); |
||||
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid); |
||||
contents.forEach(function (name) { |
||||
elem.c('content', {name: name}); |
||||
elem.c('channel', {initiator: 'true', expire:'15'}); |
||||
elem.up(); // end of channel
|
||||
elem.up(); // end of content
|
||||
}); |
||||
|
||||
this.connection.sendIQ(elem, |
||||
function (result) { |
||||
var contents = $(result).find('>conference>content').get(); |
||||
for (var i = 0; i < contents.length; i++) { |
||||
tmp = $(contents[i]).find('>channel').get(); |
||||
ob.channels[index][i] = tmp[0]; |
||||
} |
||||
ob.initiate(peer, true); |
||||
}, |
||||
function (error) { |
||||
console.warn(error); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
// update the channel description (payload-types + dtls fp) for a participant
|
||||
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) { |
||||
console.log('change allocation for', this.confid); |
||||
var change = $iq({to: this.bridgejid, type: 'set'}); |
||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); |
||||
for (channel = 0; channel < this.channels[participant].length; channel++) { |
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); |
||||
change.c('channel', {id: $(this.channels[participant][channel]).attr('id')}); |
||||
|
||||
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:'); |
||||
rtpmap.forEach(function (val) { |
||||
// TODO: too much copy-paste
|
||||
var rtpmap = SDPUtil.parse_rtpmap(val); |
||||
change.c('payload-type', rtpmap); |
||||
//
|
||||
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
||||
/* |
||||
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) { |
||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)); |
||||
for (var k = 0; k < tmp.length; k++) { |
||||
change.c('parameter', tmp[k]).up(); |
||||
} |
||||
} |
||||
*/ |
||||
change.up(); |
||||
}); |
||||
|
||||
// now add transport
|
||||
change.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'}); |
||||
var fingerprints = SDPUtil.find_lines(remoteSDP.media[channel], 'a=fingerprint:', remoteSDP.session); |
||||
fingerprints.forEach(function (line) { |
||||
tmp = SDPUtil.parse_fingerprint(line); |
||||
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0'; |
||||
change.c('fingerprint').t(tmp.fingerprint); |
||||
delete tmp.fingerprint; |
||||
line = SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:', remoteSDP.session); |
||||
if (line) { |
||||
tmp.setup = line.substr(8); |
||||
} |
||||
change.attrs(tmp); |
||||
change.up(); |
||||
}); |
||||
var candidates = SDPUtil.find_lines(remoteSDP.media[channel], 'a=candidate:', remoteSDP.session); |
||||
candidates.forEach(function (line) { |
||||
var tmp = SDPUtil.candidateToJingle(line); |
||||
change.c('candidate', tmp).up(); |
||||
}); |
||||
tmp = SDPUtil.iceparams(remoteSDP.media[channel], remoteSDP.session); |
||||
if (tmp) { |
||||
change.attrs(tmp); |
||||
|
||||
} |
||||
change.up(); // end of transport
|
||||
change.up(); // end of channel
|
||||
change.up(); // end of content
|
||||
} |
||||
this.connection.sendIQ(change, |
||||
function (res) { |
||||
console.log('got result'); |
||||
}, |
||||
function (err) { |
||||
console.log('got error'); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
// tell everyone about a new participants a=ssrc lines (isadd is true)
|
||||
// or a leaving participants a=ssrc lines
|
||||
// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
|
||||
ColibriFocus.prototype.sendSSRCUpdate = function (sdp, exclude, isadd) { |
||||
var ob = this; |
||||
this.peers.forEach(function (peerjid) { |
||||
if (peerjid == exclude) return; |
||||
console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', exclude); |
||||
if (!ob.remotessrc[peerjid]) { |
||||
// FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
|
||||
// possibly, this.remoteSSRC[session.peerjid] does not exist yet
|
||||
console.warn('do we really want to bother', peerjid, 'with updates yet?'); |
||||
} |
||||
var channel; |
||||
var peersess = ob.connection.jingle.jid2session[peerjid]; |
||||
var modify = $iq({to: peerjid, type: 'set'}) |
||||
.c('jingle', { |
||||
xmlns: 'urn:xmpp:jingle:1', |
||||
action: isadd ? 'addsource' : 'removesource', |
||||
initiator: peersess.initiator, |
||||
sid: peersess.sid |
||||
} |
||||
); |
||||
for (channel = 0; channel < sdp.media.length; channel++) { |
||||
tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:'); |
||||
modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))}); |
||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' }); |
||||
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
|
||||
tmp.forEach(function (line) { |
||||
var idx = line.indexOf(' '); |
||||
var linessrc = line.substr(0, idx).substr(7); |
||||
modify.attrs({ssrc: linessrc}); |
||||
|
||||
var kv = line.substr(idx + 1); |
||||
modify.c('parameter'); |
||||
if (kv.indexOf(':') == -1) { |
||||
modify.attrs({ name: kv }); |
||||
} else { |
||||
modify.attrs({ name: kv.split(':', 2)[0] }); |
||||
modify.attrs({ value: kv.split(':', 2)[1] }); |
||||
} |
||||
modify.up(); |
||||
}); |
||||
modify.up(); // end of source
|
||||
modify.up(); // end of content
|
||||
} |
||||
ob.connection.sendIQ(modify, |
||||
function (res) { |
||||
console.warn('got modify result'); |
||||
}, |
||||
function (err) { |
||||
console.warn('got modify error'); |
||||
} |
||||
); |
||||
}); |
||||
}; |
||||
|
||||
ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) { |
||||
var participant = this.peers.indexOf(session.peerjid); |
||||
console.log('Colibri.setRemoteDescription from', session.peerjid, participant); |
||||
var ob = this; |
||||
var remoteSDP = new SDP(''); |
||||
var tmp; |
||||
var channel; |
||||
remoteSDP.fromJingle(elem); |
||||
|
||||
// ACT 1: change allocation on bridge
|
||||
this.updateChannel(remoteSDP, participant); |
||||
|
||||
// ACT 2: tell anyone else about the new SSRCs
|
||||
this.sendSSRCUpdate(remoteSDP, session.peerjid, true); |
||||
|
||||
// ACT 3: note the SSRCs
|
||||
this.remotessrc[session.peerjid] = []; |
||||
for (channel = 0; channel < this.channels[participant].length; channel++) { |
||||
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'; |
||||
} |
||||
|
||||
// ACT 4: add new a=ssrc lines to local remotedescription
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++) { |
||||
if (!this.addssrc[channel]) this.addssrc[channel] = ''; |
||||
this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n'; |
||||
} |
||||
this.modifySources(); |
||||
}; |
||||
|
||||
// relay ice candidates to bridge using trickle
|
||||
ColibriFocus.prototype.addIceCandidate = function (session, elem) { |
||||
var ob = this; |
||||
var participant = this.peers.indexOf(session.peerjid); |
||||
console.log('change transport allocation for', this.confid, session.peerjid, participant); |
||||
var change = $iq({to: this.bridgejid, type: 'set'}); |
||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); |
||||
$(elem).each(function () { |
||||
var name = $(this).attr('name'); |
||||
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
|
||||
|
||||
change.c('content', {name: name}); |
||||
change.c('channel', {id: $(ob.channels[participant][channel]).attr('id')}); |
||||
$(this).find('>transport').each(function () { |
||||
change.c('transport', { |
||||
ufrag: $(this).attr('ufrag'), |
||||
pwd: $(this).attr('pwd'), |
||||
xmlns: $(this).attr('xmlns') |
||||
}); |
||||
|
||||
$(this).find('>candidate').each(function () { |
||||
/* not yet |
||||
if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) { |
||||
// chrome generates TCP candidates with port 0
|
||||
return; |
||||
} |
||||
*/ |
||||
var line = SDPUtil.candidateFromJingle(this); |
||||
change.c('candidate', SDPUtil.candidateToJingle(line)).up(); |
||||
}); |
||||
change.up(); // end of transport
|
||||
}); |
||||
change.up(); // end of channel
|
||||
change.up(); // end of content
|
||||
}); |
||||
// FIXME: need to check if there is at least one candidate when filtering TCP ones
|
||||
this.connection.sendIQ(change, |
||||
function (res) { |
||||
console.log('got result'); |
||||
}, |
||||
function (err) { |
||||
console.warn('got error'); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
// send our own candidate to the bridge
|
||||
ColibriFocus.prototype.sendIceCandidate = function (candidate) { |
||||
//console.log('candidate', candidate);
|
||||
if (!candidate) { |
||||
console.log('end of candidates'); |
||||
return; |
||||
} |
||||
var mycands = $iq({to: this.bridgejid, type: 'set'}); |
||||
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); |
||||
mycands.c('content', {name: candidate.sdpMid }); |
||||
mycands.c('channel', {id: $(this.mychannel[candidate.sdpMLineIndex]).attr('id')}); |
||||
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'}); |
||||
tmp = SDPUtil.candidateToJingle(candidate.candidate); |
||||
mycands.c('candidate', tmp).up(); |
||||
this.connection.sendIQ(mycands, |
||||
function (res) { |
||||
console.log('got result'); |
||||
}, |
||||
function (err) { |
||||
console.warn('got error'); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
ColibriFocus.prototype.terminate = function (session, reason) { |
||||
console.log('remote session terminated from', session.peerjid); |
||||
var participant = this.peers.indexOf(session.peerjid); |
||||
if (!this.remotessrc[session.peerjid] || participant == -1) { |
||||
return; |
||||
} |
||||
console.log('remote ssrcs:', this.remotessrc[session.peerjid]); |
||||
var ssrcs = this.remotessrc[session.peerjid]; |
||||
for (var i = 0; i < ssrcs.length; i++) { |
||||
if (!this.removessrc[i]) this.removessrc[i] = ''; |
||||
this.removessrc[i] += ssrcs[i]; |
||||
} |
||||
// remove from this.peers
|
||||
this.peers.splice(participant, 1); |
||||
// expire channel on bridge
|
||||
var change = $iq({to: this.bridgejid, type: 'set'}); |
||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid}); |
||||
for (var channel = 0; channel < this.channels[participant].length; channel++) { |
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'}); |
||||
change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'}); |
||||
change.up(); // end of channel
|
||||
change.up(); // end of content
|
||||
} |
||||
this.connection.sendIQ(change, |
||||
function (res) { |
||||
console.log('got result'); |
||||
}, |
||||
function (err) { |
||||
console.log('got error'); |
||||
} |
||||
); |
||||
// and remove from channels
|
||||
this.channels.splice(participant, 1); |
||||
|
||||
// tell everyone about the ssrcs to be removed
|
||||
var sdp = new SDP(''); |
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp); |
||||
var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid); |
||||
for (var j = 0; j < ssrcs.length; j++) { |
||||
sdp.media[j] = 'a=mid:' + contents[j] + '\r\n'; |
||||
sdp.media[j] += ssrcs[j]; |
||||
this.removessrc[j] += ssrcs[j]; |
||||
} |
||||
this.sendSSRCUpdate(sdp, session.peerjid, false); |
||||
|
||||
delete this.remotessrc[session.peerjid]; |
||||
this.modifySources(); |
||||
}; |
||||
|
||||
ColibriFocus.prototype.modifySources = function () { |
||||
var ob = this; |
||||
if (!(this.addssrc.length || this.removessrc.length)) return; |
||||
if (this.peerconnection.signalingState == 'closed') return; |
||||
|
||||
// FIXME: this is a big hack
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=2688
|
||||
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) { |
||||
console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState); |
||||
window.setTimeout(function () { ob.modifySources(); }, 250); |
||||
this.wait = true; |
||||
return; |
||||
} |
||||
if (this.wait) { |
||||
window.setTimeout(function () { ob.modifySources(); }, 2500); |
||||
this.wait = false; |
||||
return; |
||||
} |
||||
var sdp = new SDP(this.peerconnection.remoteDescription.sdp); |
||||
|
||||
// add sources
|
||||
this.addssrc.forEach(function (lines, idx) { |
||||
sdp.media[idx] += lines; |
||||
}); |
||||
this.addssrc = []; |
||||
|
||||
// remove sources
|
||||
this.removessrc.forEach(function (lines, idx) { |
||||
lines = lines.split('\r\n'); |
||||
lines.pop(); // remove empty last element;
|
||||
lines.forEach(function (line) { |
||||
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', ''); |
||||
}); |
||||
}); |
||||
this.removessrc = []; |
||||
|
||||
sdp.raw = sdp.session + sdp.media.join(''); |
||||
this.peerconnection.setRemoteDescription( |
||||
new RTCSessionDescription({type: 'offer', sdp: sdp.raw }), |
||||
function () { |
||||
console.log('setModifiedRemoteDescription ok'); |
||||
ob.peerconnection.createAnswer( |
||||
function (modifiedAnswer) { |
||||
console.log('modifiedAnswer created'); |
||||
// FIXME: pushing down an answer while ice connection state
|
||||
// is still checking is bad...
|
||||
console.log(ob.peerconnection.iceConnectionState); |
||||
ob.peerconnection.setLocalDescription(modifiedAnswer, |
||||
function () { |
||||
console.log('setModifiedLocalDescription ok'); |
||||
}, |
||||
function (error) { |
||||
console.log('setModifiedLocalDescription failed'); |
||||
} |
||||
); |
||||
}, |
||||
function (error) { |
||||
console.log('createModifiedAnswer failed'); |
||||
} |
||||
); |
||||
}, |
||||
function (error) { |
||||
console.log('setModifiedRemoteDescription failed'); |
||||
} |
||||
); |
||||
}; |
||||
|
||||
|
||||
// A colibri session is similar to a jingle session, it just implements some things differently
|
||||
// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
|
||||
function ColibriSession(me, sid, connection) { |
||||
this.me = me; |
||||
this.sid = sid; |
||||
this.connection = connection; |
||||
//this.peerconnection = null;
|
||||
//this.mychannel = null;
|
||||
//this.channels = null;
|
||||
this.peerjid = null; |
||||
|
||||
this.colibri = null; |
||||
} |
||||
|
||||
// implementation of JingleSession interface
|
||||
ColibriSession.prototype.initiate = function (peerjid, isInitiator) { |
||||
this.peerjid = peerjid; |
||||
}; |
||||
|
||||
ColibriSession.prototype.sendOffer = function (offer) { |
||||
console.log('ColibriSession.sendOffer'); |
||||
}; |
||||
|
||||
|
||||
ColibriSession.prototype.accept = function () { |
||||
console.log('ColibriSession.accept'); |
||||
}; |
||||
|
||||
ColibriSession.prototype.terminate = function (reason) { |
||||
this.colibri.terminate(this, reason); |
||||
}; |
||||
|
||||
ColibriSession.prototype.active = function () { |
||||
console.log('ColibriSession.active'); |
||||
}; |
||||
|
||||
ColibriSession.prototype.setRemoteDescription = function (elem, desctype) { |
||||
this.colibri.setRemoteDescription(this, elem, desctype); |
||||
}; |
||||
|
||||
ColibriSession.prototype.addIceCandidate = function (elem) { |
||||
this.colibri.addIceCandidate(this, elem); |
||||
}; |
||||
|
||||
ColibriSession.prototype.sendAnswer = function (sdp, provisional) { |
||||
console.log('ColibriSession.sendAnswer'); |
||||
}; |
||||
|
||||
ColibriSession.prototype.sendTerminate = function (reason, text) { |
||||
console.log('ColibriSession.sendTerminate'); |
||||
}; |
||||
@ -0,0 +1,666 @@ |
||||
/*! jQuery-Impromptu - v5.1.1 |
||||
* http://trentrichardson.com/Impromptu
|
||||
* Copyright (c) 2013 Trent Richardson; Licensed MIT */ |
||||
(function($) { |
||||
"use strict"; |
||||
|
||||
/** |
||||
* setDefaults - Sets the default options |
||||
* @param message String/Object - String of html or Object of states |
||||
* @param options Object - Options to set the prompt |
||||
* @return jQuery - container with overlay and prompt
|
||||
*/ |
||||
$.prompt = function(message, options) { |
||||
// only for backwards compat, to be removed in future version
|
||||
if(options !== undefined && options.classes !== undefined && typeof options.classes === 'string'){ |
||||
options = { box: options.classes }; |
||||
} |
||||
|
||||
$.prompt.options = $.extend({},$.prompt.defaults,options); |
||||
$.prompt.currentPrefix = $.prompt.options.prefix; |
||||
|
||||
// Be sure any previous timeouts are destroyed
|
||||
if($.prompt.timeout){ |
||||
clearTimeout($.prompt.timeout); |
||||
} |
||||
$.prompt.timeout = false; |
||||
|
||||
var opts = $.prompt.options, |
||||
$body = $(document.body), |
||||
$window = $(window); |
||||
|
||||
//build the box and fade
|
||||
var msgbox = '<div class="'+ $.prompt.options.prefix +'box '+ opts.classes.box +'">'; |
||||
if(opts.useiframe && ($('object, applet').length > 0)) { |
||||
msgbox += '<iframe src="javascript:false;" style="display:block;position:absolute;z-index:-1;" class="'+ opts.prefix +'fade '+ opts.classes.fade +'"></iframe>'; |
||||
} else { |
||||
msgbox +='<div class="'+ opts.prefix +'fade '+ opts.classes.fade +'"></div>'; |
||||
} |
||||
msgbox += '<div class="'+ opts.prefix +' '+ opts.classes.prompt +'">'+ |
||||
'<form action="javascript:false;" onsubmit="return false;" class="'+ opts.prefix +'form">'+ |
||||
'<div class="'+ opts.prefix +'close '+ opts.classes.close +'">'+ opts.closeText +'</div>'+ |
||||
'<div class="'+ opts.prefix +'states"></div>'+ |
||||
'</form>'+ |
||||
'</div>'+ |
||||
'</div>'; |
||||
|
||||
$.prompt.jqib = $(msgbox).appendTo($body); |
||||
$.prompt.jqi = $.prompt.jqib.children('.'+ opts.prefix);//.data('jqi',opts);
|
||||
$.prompt.jqif = $.prompt.jqib.children('.'+ opts.prefix +'fade'); |
||||
|
||||
//if a string was passed, convert to a single state
|
||||
if(message.constructor === String){ |
||||
message = { |
||||
state0: { |
||||
title: opts.title, |
||||
html: message, |
||||
buttons: opts.buttons, |
||||
position: opts.position, |
||||
focus: opts.focus, |
||||
submit: opts.submit |
||||
} |
||||
}; |
||||
} |
||||
|
||||
//build the states
|
||||
$.prompt.options.states = {}; |
||||
var k,v; |
||||
for(k in message){ |
||||
v = $.extend({},$.prompt.defaults.state,{name:k},message[k]); |
||||
$.prompt.addState(v.name, v); |
||||
|
||||
if($.prompt.currentStateName === ''){ |
||||
$.prompt.currentStateName = v.name; |
||||
} |
||||
} |
||||
|
||||
// Go ahead and transition to the first state. It won't be visible just yet though until we show the prompt
|
||||
var $firstState = $.prompt.jqi.find('.'+ opts.prefix +'states .'+ opts.prefix +'state').eq(0); |
||||
$.prompt.goToState($firstState.data('jqi-name')); |
||||
|
||||
//Events
|
||||
$.prompt.jqi.on('click', '.'+ opts.prefix +'buttons button', function(e){ |
||||
var $t = $(this), |
||||
$state = $t.parents('.'+ opts.prefix +'state'), |
||||
stateobj = $.prompt.options.states[$state.data('jqi-name')], |
||||
msg = $state.children('.'+ opts.prefix +'message'), |
||||
clicked = stateobj.buttons[$t.text()] || stateobj.buttons[$t.html()], |
||||
forminputs = {}; |
||||
|
||||
// if for some reason we couldn't get the value
|
||||
if(clicked === undefined){ |
||||
for(var i in stateobj.buttons){ |
||||
if(stateobj.buttons[i].title === $t.text() || stateobj.buttons[i].title === $t.html()){ |
||||
clicked = stateobj.buttons[i].value; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//collect all form element values from all states.
|
||||
$.each($.prompt.jqi.children('form').serializeArray(),function(i,obj){ |
||||
if (forminputs[obj.name] === undefined) { |
||||
forminputs[obj.name] = obj.value; |
||||
} else if (typeof forminputs[obj.name] === Array || typeof forminputs[obj.name] === 'object') { |
||||
forminputs[obj.name].push(obj.value); |
||||
} else { |
||||
forminputs[obj.name] = [forminputs[obj.name],obj.value];
|
||||
}
|
||||
}); |
||||
|
||||
// trigger an event
|
||||
var promptsubmite = new $.Event('impromptu:submit'); |
||||
promptsubmite.stateName = stateobj.name; |
||||
promptsubmite.state = $state; |
||||
$state.trigger(promptsubmite, [clicked, msg, forminputs]); |
||||
|
||||
if(!promptsubmite.isDefaultPrevented()){ |
||||
$.prompt.close(true, clicked,msg,forminputs); |
||||
} |
||||
}); |
||||
|
||||
// if the fade is clicked blink the prompt
|
||||
var fadeClicked = function(){ |
||||
if(opts.persistent){ |
||||
var offset = (opts.top.toString().indexOf('%') >= 0? ($window.height()*(parseInt(opts.top,10)/100)) : parseInt(opts.top,10)), |
||||
top = parseInt($.prompt.jqi.css('top').replace('px',''),10) - offset; |
||||
|
||||
//$window.scrollTop(top);
|
||||
$('html,body').animate({ scrollTop: top }, 'fast', function(){ |
||||
var i = 0; |
||||
$.prompt.jqib.addClass(opts.prefix +'warning'); |
||||
var intervalid = setInterval(function(){ |
||||
$.prompt.jqib.toggleClass(opts.prefix +'warning'); |
||||
if(i++ > 1){ |
||||
clearInterval(intervalid); |
||||
$.prompt.jqib.removeClass(opts.prefix +'warning'); |
||||
} |
||||
}, 100); |
||||
}); |
||||
} |
||||
else { |
||||
$.prompt.close(true); |
||||
} |
||||
}; |
||||
|
||||
// listen for esc or tab keys
|
||||
var keyPressEventHandler = function(e){ |
||||
var key = (window.event) ? event.keyCode : e.keyCode; |
||||
|
||||
//escape key closes
|
||||
if(key===27) { |
||||
fadeClicked();
|
||||
} |
||||
|
||||
//constrain tabs, tabs should iterate through the state and not leave
|
||||
if (key === 9){ |
||||
var $inputels = $('input,select,textarea,button',$.prompt.getCurrentState()); |
||||
var fwd = !e.shiftKey && e.target === $inputels[$inputels.length-1]; |
||||
var back = e.shiftKey && e.target === $inputels[0]; |
||||
if (fwd || back) { |
||||
setTimeout(function(){
|
||||
if (!$inputels){ |
||||
return; |
||||
} |
||||
var el = $inputels[back===true ? $inputels.length-1 : 0]; |
||||
|
||||
if (el){ |
||||
el.focus(); |
||||
} |
||||
},10); |
||||
return false; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
$.prompt.position(); |
||||
$.prompt.style(); |
||||
|
||||
$.prompt.jqif.click(fadeClicked); |
||||
$window.resize({animate:false}, $.prompt.position); |
||||
$.prompt.jqi.find('.'+ opts.prefix +'close').click($.prompt.close); |
||||
$.prompt.jqib.on("keydown",keyPressEventHandler) |
||||
.on('impromptu:loaded', opts.loaded) |
||||
.on('impromptu:close', opts.close) |
||||
.on('impromptu:statechanging', opts.statechanging) |
||||
.on('impromptu:statechanged', opts.statechanged); |
||||
|
||||
// Show it
|
||||
$.prompt.jqif[opts.show](opts.overlayspeed); |
||||
$.prompt.jqi[opts.show](opts.promptspeed, function(){ |
||||
$.prompt.jqib.trigger('impromptu:loaded'); |
||||
}); |
||||
|
||||
// Timeout
|
||||
if(opts.timeout > 0){ |
||||
$.prompt.timeout = setTimeout(function(){ $.prompt.close(true); },opts.timeout); |
||||
} |
||||
|
||||
return $.prompt.jqib; |
||||
}; |
||||
|
||||
$.prompt.defaults = { |
||||
prefix:'jqi', |
||||
classes: { |
||||
box: '', |
||||
fade: '', |
||||
prompt: '', |
||||
close: '', |
||||
title: '', |
||||
message: '', |
||||
buttons: '', |
||||
button: '', |
||||
defaultButton: '' |
||||
}, |
||||
title: '', |
||||
closeText: '×', |
||||
buttons: { |
||||
Ok: true |
||||
}, |
||||
loaded: function(e){}, |
||||
submit: function(e,v,m,f){}, |
||||
close: function(e,v,m,f){}, |
||||
statechanging: function(e, from, to){}, |
||||
statechanged: function(e, to){}, |
||||
opacity: 0.6, |
||||
zIndex: 999, |
||||
overlayspeed: 'slow', |
||||
promptspeed: 'fast', |
||||
show: 'fadeIn', |
||||
focus: 0, |
||||
defaultButton: 0, |
||||
useiframe: false, |
||||
top: '15%', |
||||
position: {
|
||||
container: null,
|
||||
x: null,
|
||||
y: null, |
||||
arrow: null, |
||||
width: null |
||||
}, |
||||
persistent: true, |
||||
timeout: 0, |
||||
states: {}, |
||||
state: { |
||||
name: null, |
||||
title: '', |
||||
html: '', |
||||
buttons: { |
||||
Ok: true |
||||
}, |
||||
focus: 0, |
||||
defaultButton: 0, |
||||
position: {
|
||||
container: null,
|
||||
x: null,
|
||||
y: null, |
||||
arrow: null, |
||||
width: null |
||||
}, |
||||
submit: function(e,v,m,f){ |
||||
return true; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* currentPrefix String - At any time this show be the prefix
|
||||
* of the current prompt ex: "jqi" |
||||
*/ |
||||
$.prompt.currentPrefix = $.prompt.defaults.prefix; |
||||
|
||||
/** |
||||
* currentStateName String - At any time this is the current state |
||||
* of the current prompt ex: "state0" |
||||
*/ |
||||
$.prompt.currentStateName = ""; |
||||
|
||||
/** |
||||
* setDefaults - Sets the default options |
||||
* @param o Object - Options to set as defaults |
||||
* @return void |
||||
*/ |
||||
$.prompt.setDefaults = function(o) { |
||||
$.prompt.defaults = $.extend({}, $.prompt.defaults, o); |
||||
}; |
||||
|
||||
/** |
||||
* setStateDefaults - Sets the default options for a state |
||||
* @param o Object - Options to set as defaults |
||||
* @return void |
||||
*/ |
||||
$.prompt.setStateDefaults = function(o) { |
||||
$.prompt.defaults.state = $.extend({}, $.prompt.defaults.state, o); |
||||
}; |
||||
|
||||
/** |
||||
* position - Repositions the prompt (Used internally) |
||||
* @return void |
||||
*/ |
||||
$.prompt.position = function(e){ |
||||
var restoreFx = $.fx.off, |
||||
$state = $.prompt.getCurrentState(), |
||||
stateObj = $.prompt.options.states[$state.data('jqi-name')], |
||||
pos = stateObj? stateObj.position : undefined, |
||||
$window = $(window), |
||||
bodyHeight = document.body.scrollHeight, //$(document.body).outerHeight(true),
|
||||
windowHeight = $(window).height(), |
||||
documentHeight = $(document).height(), |
||||
height = bodyHeight > windowHeight ? bodyHeight : windowHeight, |
||||
top = parseInt($window.scrollTop(),10) + ($.prompt.options.top.toString().indexOf('%') >= 0?
|
||||
(windowHeight*(parseInt($.prompt.options.top,10)/100)) : parseInt($.prompt.options.top,10)); |
||||
|
||||
// when resizing the window turn off animation
|
||||
if(e !== undefined && e.data.animate === false){ |
||||
$.fx.off = true; |
||||
} |
||||
|
||||
$.prompt.jqib.css({ |
||||
position: "absolute", |
||||
height: height, |
||||
width: "100%", |
||||
top: 0, |
||||
left: 0, |
||||
right: 0, |
||||
bottom: 0 |
||||
}); |
||||
$.prompt.jqif.css({ |
||||
position: "fixed", |
||||
height: height, |
||||
width: "100%", |
||||
top: 0, |
||||
left: 0, |
||||
right: 0, |
||||
bottom: 0 |
||||
}); |
||||
|
||||
// tour positioning
|
||||
if(pos && pos.container){ |
||||
var offset = $(pos.container).offset(); |
||||
|
||||
if($.isPlainObject(offset) && offset.top !== undefined){ |
||||
$.prompt.jqi.css({ |
||||
position: "absolute" |
||||
}); |
||||
$.prompt.jqi.animate({ |
||||
top: offset.top + pos.y, |
||||
left: offset.left + pos.x, |
||||
marginLeft: 0, |
||||
width: (pos.width !== undefined)? pos.width : null |
||||
}); |
||||
top = (offset.top + pos.y) - ($.prompt.options.top.toString().indexOf('%') >= 0? (windowHeight*(parseInt($.prompt.options.top,10)/100)) : parseInt($.prompt.options.top,10)); |
||||
$('html,body').animate({ scrollTop: top }, 'slow', 'swing', function(){}); |
||||
} |
||||
} |
||||
// custom state width animation
|
||||
else if(pos && pos.width){ |
||||
$.prompt.jqi.css({ |
||||
position: "absolute", |
||||
left: '50%' |
||||
}); |
||||
$.prompt.jqi.animate({ |
||||
top: pos.y || top, |
||||
left: pos.x || '50%', |
||||
marginLeft: ((pos.width/2)*-1), |
||||
width: pos.width |
||||
}); |
||||
} |
||||
// standard prompt positioning
|
||||
else{ |
||||
$.prompt.jqi.css({ |
||||
position: "absolute", |
||||
top: top, |
||||
left: '50%',//$window.width()/2,
|
||||
marginLeft: (($.prompt.jqi.outerWidth(false)/2)*-1) |
||||
}); |
||||
} |
||||
|
||||
// restore fx settings
|
||||
if(e !== undefined && e.data.animate === false){ |
||||
$.fx.off = restoreFx; |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* style - Restyles the prompt (Used internally) |
||||
* @return void |
||||
*/ |
||||
$.prompt.style = function(){ |
||||
$.prompt.jqif.css({ |
||||
zIndex: $.prompt.options.zIndex, |
||||
display: "none", |
||||
opacity: $.prompt.options.opacity |
||||
}); |
||||
$.prompt.jqi.css({ |
||||
zIndex: $.prompt.options.zIndex+1, |
||||
display: "none" |
||||
}); |
||||
$.prompt.jqib.css({ |
||||
zIndex: $.prompt.options.zIndex |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* get - Get the prompt |
||||
* @return jQuery - the prompt |
||||
*/ |
||||
$.prompt.get = function(state) { |
||||
return $('.'+ $.prompt.currentPrefix); |
||||
}; |
||||
|
||||
/** |
||||
* addState - Injects a state into the prompt |
||||
* @param statename String - Name of the state |
||||
* @param stateobj Object - options for the state |
||||
* @param afterState String - selector of the state to insert after |
||||
* @return jQuery - the newly created state |
||||
*/ |
||||
$.prompt.addState = function(statename, stateobj, afterState) { |
||||
var state = "", |
||||
$state = null, |
||||
arrow = "", |
||||
title = "", |
||||
opts = $.prompt.options, |
||||
$jqistates = $('.'+ $.prompt.currentPrefix +'states'), |
||||
defbtn,k,v,i=0; |
||||
|
||||
stateobj = $.extend({},$.prompt.defaults.state, {name:statename}, stateobj); |
||||
|
||||
if(stateobj.position.arrow !== null){ |
||||
arrow = '<div class="'+ opts.prefix + 'arrow '+ opts.prefix + 'arrow'+ stateobj.position.arrow +'"></div>'; |
||||
} |
||||
if(stateobj.title && stateobj.title !== ''){ |
||||
title = '<div class="lead '+ opts.prefix + 'title '+ opts.classes.title +'">'+ stateobj.title +'</div>'; |
||||
} |
||||
state += '<div id="'+ opts.prefix +'state_'+ statename +'" class="'+ opts.prefix + 'state" data-jqi-name="'+ statename +'" style="display:none;">'+
|
||||
arrow + title + |
||||
'<div class="'+ opts.prefix +'message '+ opts.classes.message +'">' + stateobj.html +'</div>'+ |
||||
'<div class="'+ opts.prefix +'buttons '+ opts.classes.buttons +'"'+ ($.isEmptyObject(stateobj.buttons)? 'style="display:none;"':'') +'>'; |
||||
|
||||
for(k in stateobj.buttons){ |
||||
v = stateobj.buttons[k], |
||||
defbtn = stateobj.focus === i || (isNaN(stateobj.focus) && stateobj.defaultButton === i) ? ($.prompt.currentPrefix + 'defaultbutton ' + opts.classes.defaultButton) : ''; |
||||
|
||||
if(typeof v === 'object'){ |
||||
state += '<button class="'+ opts.classes.button +' '+ defbtn; |
||||
|
||||
if(typeof v.classes !== "undefined"){ |
||||
state += ' '+ ($.isArray(v.classes)? v.classes.join(' ') : v.classes) + ' '; |
||||
} |
||||
|
||||
state += '" name="' + opts.prefix + '_' + statename + '_button' + v.title.replace(/[^a-z0-9]+/gi,'') + '" id="' + opts.prefix + '_' + statename + '_button' + v.title.replace(/[^a-z0-9]+/gi,'') + '" value="' + v.value + '">' + v.title + '</button>'; |
||||
|
||||
} else { |
||||
state += '<button class="'+ opts.classes.button +' '+ defbtn +'" name="' + opts.prefix + '_' + statename + '_button' + k + '" id="' + opts.prefix + '_' + statename + '_button' + k + '" value="' + v + '">' + k + '</button>'; |
||||
|
||||
} |
||||
i++; |
||||
} |
||||
state += '</div></div>'; |
||||
|
||||
$state = $(state); |
||||
|
||||
$state.on('impromptu:submit', stateobj.submit); |
||||
|
||||
if(afterState !== undefined){ |
||||
$jqistates.find('#'+ $.prompt.currentPrefix +'state_'+ afterState).after($state); |
||||
} |
||||
else{ |
||||
$jqistates.append($state); |
||||
} |
||||
|
||||
$.prompt.options.states[statename] = stateobj; |
||||
|
||||
return $state; |
||||
}; |
||||
|
||||
/** |
||||
* removeState - Removes a state from the promt |
||||
* @param state String - Name of the state |
||||
* @return Boolean - returns true on success, false on failure |
||||
*/ |
||||
$.prompt.removeState = function(state) { |
||||
var $state = $.prompt.getState(state), |
||||
rm = function(){ $state.remove(); }; |
||||
|
||||
if($state.length === 0){ |
||||
return false; |
||||
} |
||||
|
||||
// transition away from it before deleting
|
||||
if($state.is(':visible')){ |
||||
if($state.next().length > 0){ |
||||
$.prompt.nextState(rm); |
||||
} |
||||
else{ |
||||
$.prompt.prevState(rm); |
||||
} |
||||
} |
||||
else{ |
||||
$state.slideUp('slow', rm); |
||||
} |
||||
|
||||
return true; |
||||
}; |
||||
|
||||
/** |
||||
* getState - Get the state by its name |
||||
* @param state String - Name of the state |
||||
* @return jQuery - the state |
||||
*/ |
||||
$.prompt.getState = function(state) { |
||||
return $('#'+ $.prompt.currentPrefix +'state_'+ state); |
||||
}; |
||||
$.prompt.getStateContent = function(state) { |
||||
return $.prompt.getState(state); |
||||
}; |
||||
|
||||
/** |
||||
* getCurrentState - Get the current visible state |
||||
* @return jQuery - the current visible state |
||||
*/ |
||||
$.prompt.getCurrentState = function() { |
||||
return $.prompt.getState($.prompt.getCurrentStateName()); |
||||
}; |
||||
|
||||
/** |
||||
* getCurrentStateName - Get the name of the current visible state |
||||
* @return String - the current visible state's name |
||||
*/ |
||||
$.prompt.getCurrentStateName = function() { |
||||
return $.prompt.currentStateName; |
||||
}; |
||||
|
||||
/** |
||||
* goToState - Goto the specified state |
||||
* @param state String - name of the state to transition to |
||||
* @param subState Boolean - true to be a sub state within the currently open state |
||||
* @param callback Function - called when the transition is complete |
||||
* @return jQuery - the newly active state |
||||
*/ |
||||
$.prompt.goToState = function(state, subState, callback) { |
||||
var $jqi = $.prompt.get(), |
||||
jqiopts = $.prompt.options, |
||||
$state = $.prompt.getState(state), |
||||
stateobj = jqiopts.states[$state.data('jqi-name')], |
||||
promptstatechanginge = new $.Event('impromptu:statechanging'); |
||||
|
||||
// subState can be ommitted
|
||||
if(typeof subState === 'function'){ |
||||
callback = subState; |
||||
subState = false; |
||||
} |
||||
|
||||
$.prompt.jqib.trigger(promptstatechanginge, [$.prompt.getCurrentStateName(), state]); |
||||
|
||||
if(!promptstatechanginge.isDefaultPrevented() && $state.length > 0){ |
||||
$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'parentstate').removeClass($.prompt.currentPrefix +'parentstate'); |
||||
|
||||
if(subState){ // hide any open substates
|
||||
// get rid of any substates
|
||||
$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'substate').not($state) |
||||
.slideUp(jqiopts.promptspeed) |
||||
.removeClass('.'+ $.prompt.currentPrefix +'substate') |
||||
.find('.'+ $.prompt.currentPrefix +'arrow').hide(); |
||||
|
||||
// add parent state class so it can be visible, but blocked
|
||||
$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'state:visible').addClass($.prompt.currentPrefix +'parentstate'); |
||||
|
||||
// add substate class so we know it will be smaller
|
||||
$state.addClass($.prompt.currentPrefix +'substate'); |
||||
} |
||||
else{ // hide any open states
|
||||
$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'state').not($state) |
||||
.slideUp(jqiopts.promptspeed) |
||||
.find('.'+ $.prompt.currentPrefix +'arrow').hide(); |
||||
} |
||||
$.prompt.currentStateName = stateobj.name; |
||||
|
||||
$state.slideDown(jqiopts.promptspeed,function(){ |
||||
var $t = $(this); |
||||
|
||||
// if focus is a selector, find it, else its button index
|
||||
if(typeof(stateobj.focus) === 'string'){ |
||||
$t.find(stateobj.focus).eq(0).focus(); |
||||
} |
||||
else{ |
||||
$t.find('.'+ $.prompt.currentPrefix +'defaultbutton').focus(); |
||||
} |
||||
|
||||
$t.find('.'+ $.prompt.currentPrefix +'arrow').show(jqiopts.promptspeed); |
||||
|
||||
if (typeof callback === 'function'){ |
||||
$.prompt.jqib.on('impromptu:statechanged', callback); |
||||
} |
||||
$.prompt.jqib.trigger('impromptu:statechanged', [state]); |
||||
if (typeof callback === 'function'){ |
||||
$.prompt.jqib.off('impromptu:statechanged', callback); |
||||
} |
||||
}); |
||||
if(!subState){ |
||||
$.prompt.position(); |
||||
} |
||||
} |
||||
return $state; |
||||
}; |
||||
|
||||
/** |
||||
* nextState - Transition to the next state |
||||
* @param callback Function - called when the transition is complete |
||||
* @return jQuery - the newly active state |
||||
*/ |
||||
$.prompt.nextState = function(callback) { |
||||
var $next = $('#'+ $.prompt.currentPrefix +'state_'+ $.prompt.getCurrentStateName()).next(); |
||||
return $.prompt.goToState( $next.attr('id').replace($.prompt.currentPrefix +'state_',''), callback ); |
||||
}; |
||||
|
||||
/** |
||||
* prevState - Transition to the previous state |
||||
* @param callback Function - called when the transition is complete |
||||
* @return jQuery - the newly active state |
||||
*/ |
||||
$.prompt.prevState = function(callback) { |
||||
var $prev = $('#'+ $.prompt.currentPrefix +'state_'+ $.prompt.getCurrentStateName()).prev(); |
||||
$.prompt.goToState( $prev.attr('id').replace($.prompt.currentPrefix +'state_',''), callback ); |
||||
}; |
||||
|
||||
/** |
||||
* close - Closes the prompt |
||||
* @param callback Function - called when the transition is complete |
||||
* @param clicked String - value of the button clicked (only used internally) |
||||
* @param msg jQuery - The state message body (only used internally) |
||||
* @param forvals Object - key/value pairs of all form field names and values (only used internally) |
||||
* @return jQuery - the newly active state |
||||
*/ |
||||
$.prompt.close = function(callCallback, clicked, msg, formvals){ |
||||
if($.prompt.timeout){ |
||||
clearTimeout($.prompt.timeout); |
||||
$.prompt.timeout = false; |
||||
} |
||||
|
||||
$.prompt.jqib.fadeOut('fast',function(){ |
||||
|
||||
if(callCallback) { |
||||
$.prompt.jqib.trigger('impromptu:close', [clicked,msg,formvals]); |
||||
} |
||||
$.prompt.jqib.remove(); |
||||
|
||||
$(window).off('resize',$.prompt.position); |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* Enable using $('.selector').prompt({}); |
||||
* This will grab the html within the prompt as the prompt message |
||||
*/ |
||||
$.fn.prompt = function(options){ |
||||
if(options === undefined){ |
||||
options = {}; |
||||
} |
||||
if(options.withDataAndEvents === undefined){ |
||||
options.withDataAndEvents = false; |
||||
} |
||||
|
||||
$.prompt($(this).clone(options.withDataAndEvents).html(),options); |
||||
}; |
||||
|
||||
})(jQuery); |
||||
@ -0,0 +1,250 @@ |
||||
/*! |
||||
Autosize v1.18.1 - 2013-11-05 |
||||
Automatically adjust textarea height based on user input. |
||||
(c) 2013 Jack Moore - http://www.jacklmoore.com/autosize
|
||||
license: http://www.opensource.org/licenses/mit-license.php
|
||||
*/ |
||||
(function ($) { |
||||
var |
||||
defaults = { |
||||
className: 'autosizejs', |
||||
append: '', |
||||
callback: false, |
||||
resizeDelay: 10 |
||||
}, |
||||
|
||||
// border:0 is unnecessary, but avoids a bug in Firefox on OSX
|
||||
copy = '<textarea tabindex="-1" style="position:absolute; top:-999px; left:0; right:auto; bottom:auto; border:0; padding: 0; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden; transition:none; -webkit-transition:none; -moz-transition:none;"/>', |
||||
|
||||
// line-height is conditionally included because IE7/IE8/old Opera do not return the correct value.
|
||||
typographyStyles = [ |
||||
'fontFamily', |
||||
'fontSize', |
||||
'fontWeight', |
||||
'fontStyle', |
||||
'letterSpacing', |
||||
'textTransform', |
||||
'wordSpacing', |
||||
'textIndent' |
||||
], |
||||
|
||||
// to keep track which textarea is being mirrored when adjust() is called.
|
||||
mirrored, |
||||
|
||||
// the mirror element, which is used to calculate what size the mirrored element should be.
|
||||
mirror = $(copy).data('autosize', true)[0]; |
||||
|
||||
// test that line-height can be accurately copied.
|
||||
mirror.style.lineHeight = '99px'; |
||||
if ($(mirror).css('lineHeight') === '99px') { |
||||
typographyStyles.push('lineHeight'); |
||||
} |
||||
mirror.style.lineHeight = ''; |
||||
|
||||
$.fn.autosize = function (options) { |
||||
if (!this.length) { |
||||
return this; |
||||
} |
||||
|
||||
options = $.extend({}, defaults, options || {}); |
||||
|
||||
if (mirror.parentNode !== document.body) { |
||||
$(document.body).append(mirror); |
||||
} |
||||
|
||||
return this.each(function () { |
||||
var |
||||
ta = this, |
||||
$ta = $(ta), |
||||
maxHeight, |
||||
minHeight, |
||||
boxOffset = 0, |
||||
callback = $.isFunction(options.callback), |
||||
originalStyles = { |
||||
height: ta.style.height, |
||||
overflow: ta.style.overflow, |
||||
overflowY: ta.style.overflowY, |
||||
wordWrap: ta.style.wordWrap, |
||||
resize: ta.style.resize |
||||
}, |
||||
timeout, |
||||
width = $ta.width(); |
||||
|
||||
if ($ta.data('autosize')) { |
||||
// exit if autosize has already been applied, or if the textarea is the mirror element.
|
||||
return; |
||||
} |
||||
$ta.data('autosize', true); |
||||
|
||||
if ($ta.css('box-sizing') === 'border-box' || $ta.css('-moz-box-sizing') === 'border-box' || $ta.css('-webkit-box-sizing') === 'border-box'){ |
||||
boxOffset = $ta.outerHeight() - $ta.height(); |
||||
} |
||||
|
||||
// IE8 and lower return 'auto', which parses to NaN, if no min-height is set.
|
||||
minHeight = Math.max(parseInt($ta.css('minHeight'), 10) - boxOffset || 0, $ta.height()); |
||||
|
||||
$ta.css({ |
||||
overflow: 'hidden', |
||||
overflowY: 'hidden', |
||||
wordWrap: 'break-word', // horizontal overflow is hidden, so break-word is necessary for handling words longer than the textarea width
|
||||
resize: ($ta.css('resize') === 'none' || $ta.css('resize') === 'vertical') ? 'none' : 'horizontal' |
||||
}); |
||||
|
||||
// The mirror width must exactly match the textarea width, so using getBoundingClientRect because it doesn't round the sub-pixel value.
|
||||
function setWidth() { |
||||
var style, width; |
||||
|
||||
if ('getComputedStyle' in window) { |
||||
style = window.getComputedStyle(ta, null); |
||||
width = ta.getBoundingClientRect().width; |
||||
|
||||
$.each(['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'], function(i,val){ |
||||
width -= parseInt(style[val],10); |
||||
}); |
||||
|
||||
mirror.style.width = width + 'px'; |
||||
} |
||||
else { |
||||
// window.getComputedStyle, getBoundingClientRect returning a width are unsupported and unneeded in IE8 and lower.
|
||||
mirror.style.width = Math.max($ta.width(), 0) + 'px'; |
||||
} |
||||
} |
||||
|
||||
function initMirror() { |
||||
var styles = {}; |
||||
|
||||
mirrored = ta; |
||||
mirror.className = options.className; |
||||
maxHeight = parseInt($ta.css('maxHeight'), 10); |
||||
|
||||
// mirror is a duplicate textarea located off-screen that
|
||||
// is automatically updated to contain the same text as the
|
||||
// original textarea. mirror always has a height of 0.
|
||||
// This gives a cross-browser supported way getting the actual
|
||||
// height of the text, through the scrollTop property.
|
||||
$.each(typographyStyles, function(i,val){ |
||||
styles[val] = $ta.css(val); |
||||
}); |
||||
$(mirror).css(styles); |
||||
|
||||
setWidth(); |
||||
|
||||
// Chrome-specific fix:
|
||||
// When the textarea y-overflow is hidden, Chrome doesn't reflow the text to account for the space
|
||||
// made available by removing the scrollbar. This workaround triggers the reflow for Chrome.
|
||||
if (window.chrome) { |
||||
var width = ta.style.width; |
||||
ta.style.width = '0px'; |
||||
var ignore = ta.offsetWidth; |
||||
ta.style.width = width; |
||||
} |
||||
} |
||||
|
||||
// Using mainly bare JS in this function because it is going
|
||||
// to fire very often while typing, and needs to very efficient.
|
||||
function adjust() { |
||||
var height, original; |
||||
|
||||
if (mirrored !== ta) { |
||||
initMirror(); |
||||
} else { |
||||
setWidth(); |
||||
} |
||||
|
||||
mirror.value = ta.value + options.append; |
||||
mirror.style.overflowY = ta.style.overflowY; |
||||
original = parseInt(ta.style.height,10); |
||||
|
||||
// Setting scrollTop to zero is needed in IE8 and lower for the next step to be accurately applied
|
||||
mirror.scrollTop = 0; |
||||
|
||||
mirror.scrollTop = 9e4; |
||||
|
||||
// Using scrollTop rather than scrollHeight because scrollHeight is non-standard and includes padding.
|
||||
height = mirror.scrollTop; |
||||
|
||||
if (maxHeight && height > maxHeight) { |
||||
ta.style.overflowY = 'scroll'; |
||||
height = maxHeight; |
||||
} else { |
||||
ta.style.overflowY = 'hidden'; |
||||
if (height < minHeight) { |
||||
height = minHeight; |
||||
} |
||||
} |
||||
|
||||
height += boxOffset; |
||||
|
||||
if (original !== height) { |
||||
ta.style.height = height + 'px'; |
||||
if (callback) { |
||||
options.callback.call(ta,ta); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function resize () { |
||||
clearTimeout(timeout); |
||||
timeout = setTimeout(function(){ |
||||
var newWidth = $ta.width(); |
||||
|
||||
if (newWidth !== width) { |
||||
width = newWidth; |
||||
adjust(); |
||||
} |
||||
}, parseInt(options.resizeDelay,10)); |
||||
} |
||||
|
||||
if ('onpropertychange' in ta) { |
||||
if ('oninput' in ta) { |
||||
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
|
||||
// so binding to onkeyup to catch most of those occasions. There is no way that I
|
||||
// know of to detect something like 'cut' in IE9.
|
||||
$ta.on('input.autosize keyup.autosize', adjust); |
||||
} else { |
||||
// IE7 / IE8
|
||||
$ta.on('propertychange.autosize', function(){ |
||||
if(event.propertyName === 'value'){ |
||||
adjust(); |
||||
} |
||||
}); |
||||
} |
||||
} else { |
||||
// Modern Browsers
|
||||
$ta.on('input.autosize', adjust); |
||||
} |
||||
|
||||
// Set options.resizeDelay to false if using fixed-width textarea elements.
|
||||
// Uses a timeout and width check to reduce the amount of times adjust needs to be called after window resize.
|
||||
|
||||
if (options.resizeDelay !== false) { |
||||
$(window).on('resize.autosize', resize); |
||||
} |
||||
|
||||
// Event for manual triggering if needed.
|
||||
// Should only be needed when the value of the textarea is changed through JavaScript rather than user input.
|
||||
$ta.on('autosize.resize', adjust); |
||||
|
||||
// Event for manual triggering that also forces the styles to update as well.
|
||||
// Should only be needed if one of typography styles of the textarea change, and the textarea is already the target of the adjust method.
|
||||
$ta.on('autosize.resizeIncludeStyle', function() { |
||||
mirrored = null; |
||||
adjust(); |
||||
}); |
||||
|
||||
$ta.on('autosize.destroy', function(){ |
||||
mirrored = null; |
||||
clearTimeout(timeout); |
||||
$(window).off('resize', resize); |
||||
$ta |
||||
.off('autosize') |
||||
.off('.autosize') |
||||
.css(originalStyles) |
||||
.removeData('autosize'); |
||||
}); |
||||
|
||||
// Call adjust in case the textarea already contains text.
|
||||
adjust(); |
||||
}); |
||||
}; |
||||
}(window.jQuery || window.$)); // jQuery or jQuery-like library, such as Zepto
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,139 @@ |
||||
/* jshint -W117 */ |
||||
/* a simple MUC connection plugin |
||||
* can only handle a single MUC room |
||||
*/ |
||||
Strophe.addConnectionPlugin('emuc', { |
||||
connection: null, |
||||
roomjid: null, |
||||
myroomjid: null, |
||||
members: {}, |
||||
isOwner: false, |
||||
init: function (conn) { |
||||
this.connection = conn; |
||||
}, |
||||
doJoin: function (jid, password) { |
||||
this.myroomjid = jid; |
||||
if (!this.roomjid) { |
||||
this.roomjid = Strophe.getBareJidFromJid(jid); |
||||
// add handlers (just once)
|
||||
this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true}); |
||||
this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true}); |
||||
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true}); |
||||
this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true}); |
||||
} |
||||
|
||||
var join = $pres({to: this.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}); |
||||
if (password !== null) { |
||||
join.c('password').t(password); |
||||
} |
||||
this.connection.send(join); |
||||
}, |
||||
onPresence: function (pres) { |
||||
var from = pres.getAttribute('from'); |
||||
var type = pres.getAttribute('type'); |
||||
if (type != null) { |
||||
return true; |
||||
} |
||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) { |
||||
// http://xmpp.org/extensions/xep-0045.html#createroom-instant
|
||||
this.isOwner = true; |
||||
var create = $iq({type: 'set', to: this.roomjid}) |
||||
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}) |
||||
.c('x', {xmlns: 'jabber:x:data', type: 'submit'}); |
||||
this.connection.send(create); // fire away
|
||||
} |
||||
|
||||
var member = {}; |
||||
member.show = $(pres).find('>show').text(); |
||||
member.status = $(pres).find('>status').text(); |
||||
var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item'); |
||||
member.affilication = tmp.attr('affiliation'); |
||||
member.role = tmp.attr('role'); |
||||
if (from == this.myroomjid) { |
||||
$(document).trigger('joined.muc', [from, member]); |
||||
} else if (this.members[from] === undefined) { |
||||
// new participant
|
||||
this.members[from] = member; |
||||
$(document).trigger('entered.muc', [from, member]); |
||||
} else { |
||||
console.log('presence change from', from); |
||||
} |
||||
return true; |
||||
}, |
||||
onPresenceUnavailable: function (pres) { |
||||
var from = pres.getAttribute('from'); |
||||
delete this.members[from]; |
||||
$(document).trigger('left.muc', [from]); |
||||
return true; |
||||
}, |
||||
onPresenceError: function (pres) { |
||||
var from = pres.getAttribute('from'); |
||||
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) { |
||||
$(document).trigger('passwordrequired.muc', [from]); |
||||
|
||||
// FIXME: remove once moved to passwordrequired which should reuse dojoin
|
||||
var ob = this; |
||||
window.setTimeout(function () { |
||||
var given = window.prompt('Password required'); |
||||
if (given != null) { |
||||
// FIXME: reuse doJoin?
|
||||
ob.connection.send($pres({to: ob.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}).c('password').t(given)); |
||||
} else { |
||||
// user aborted
|
||||
} |
||||
}, 50); |
||||
} else { |
||||
console.warn('onPresError ', pres); |
||||
} |
||||
return true; |
||||
}, |
||||
sendMessage: function (body, nickname) { |
||||
var msg = $msg({to: this.roomjid, type: 'groupchat'}); |
||||
msg.c('body', body).up(); |
||||
if (nickname) { |
||||
msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up(); |
||||
} |
||||
this.connection.send(msg); |
||||
}, |
||||
onMessage: function (msg) { |
||||
var txt = $(msg).find('>body').text(); |
||||
// TODO: <subject/>
|
||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
||||
var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(msg.getAttribute('from')); |
||||
if (txt) { |
||||
console.log('chat', nick, txt); |
||||
|
||||
updateChatConversation(nick, txt); |
||||
} |
||||
return true; |
||||
}, |
||||
lockRoom: function (key) { |
||||
//http://xmpp.org/extensions/xep-0045.html#roomconfig
|
||||
var ob = this; |
||||
this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}), |
||||
function (res) { |
||||
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) { |
||||
var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}); |
||||
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'}); |
||||
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up(); |
||||
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up(); |
||||
// FIXME: is muc#roomconfig_passwordprotectedroom required?
|
||||
this.connection.sendIQ(formsubmit, |
||||
function (res) { |
||||
console.log('set room password'); |
||||
}, |
||||
function (err) { |
||||
console.warn('setting password failed', err); |
||||
} |
||||
); |
||||
} else { |
||||
console.warn('room passwords not supported'); |
||||
} |
||||
}, |
||||
function (err) { |
||||
console.warn('setting password failed', err); |
||||
} |
||||
); |
||||
} |
||||
}); |
||||
|
||||
@ -0,0 +1 @@ |
||||
Sorry, webrtc is required for this and your browser does not seem to support it. |
||||
Loading…
Reference in new issue