initial commit
This commit is contained in:
parent
bfd9f2f99c
commit
62530ef123
1
LICENSE
1
LICENSE
|
@ -1,6 +1,7 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2013 ESTOS GmbH
|
Copyright (c) 2013 ESTOS GmbH
|
||||||
|
Copyright (c) 2013 BlueJimp SARL
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
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
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
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