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