Adds mute and kick functoinality avaialable for the focus of the conference.

This commit is contained in:
yanas 2014-05-12 00:41:58 +02:00
parent ca1fc93b2d
commit 56424df0a0
11 changed files with 384 additions and 31 deletions

168
app.js
View File

@ -15,6 +15,7 @@ var ssrc2jid = {};
*/ */
var ssrc2videoType = {}; var ssrc2videoType = {};
var videoSrcToSsrc = {}; var videoSrcToSsrc = {};
var mutedAudios = {};
var localVideoSrc = null; var localVideoSrc = null;
var flipXLocalVideo = true; var flipXLocalVideo = true;
@ -35,6 +36,8 @@ var getVideoPosition;
/* window.onbeforeunload = closePageWarning; */ /* window.onbeforeunload = closePageWarning; */
var preMuted = false;
function init() { function init() {
RTC = setupRTC(); RTC = setupRTC();
if (RTC === null) { if (RTC === null) {
@ -179,6 +182,10 @@ function change_local_audio(stream) {
RTC.attachMediaStream($('#localAudio'), stream); RTC.attachMediaStream($('#localAudio'), stream);
document.getElementById('localAudio').autoplay = true; document.getElementById('localAudio').autoplay = true;
document.getElementById('localAudio').volume = 0; document.getElementById('localAudio').volume = 0;
if (preMuted) {
toggleAudio();
preMuted = false;
}
} }
function change_local_video(stream, flipX) { function change_local_video(stream, flipX) {
@ -485,8 +492,13 @@ $(document).bind('callactive.jingle', function (event, videoelem, sid) {
} }
}); });
$(document).bind('callterminated.jingle', function (event, sid, reason) { $(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
// FIXME // Leave the room if my call has been remotely terminated.
if (connection.emuc.joined && focus == null && reason === 'kick') {
connection.emuc.doLeave();
openMessageDialog( "Session Terminated",
"Ouch! You have been kicked out of the meet!");
}
}); });
$(document).bind('setLocalDescription.jingle', function (event, sid) { $(document).bind('setLocalDescription.jingle', function (event, sid) {
@ -579,15 +591,25 @@ $(document).bind('entered.muc', function (event, jid, info, pres) {
}); });
$(document).bind('left.muc', function (event, jid) { $(document).bind('left.muc', function (event, jid) {
console.log('left', jid); console.log('left.muc', jid);
connection.jingle.terminateByJid(jid); // Need to call this with a slight delay, otherwise the element couldn't be
var container = document.getElementById('participant_' + Strophe.getResourceFromJid(jid)); // found for some reason.
window.setTimeout(function () {
var container = document.getElementById(
'participant_' + Strophe.getResourceFromJid(jid));
if (container) { if (container) {
// hide here, wait for video to close before removing // hide here, wait for video to close before removing
$(container).hide(); $(container).hide();
resizeThumbnails(); resizeThumbnails();
} }
if (focus === null && connection.emuc.myroomjid === connection.emuc.list_members[0]) { }, 10);
connection.jingle.terminateByJid(jid);
if (focus == null
&& jid !== connection.emuc.myroomjid
&& connection.emuc.myroomjid === connection.emuc.list_members[0]
&& connection.emuc.list_members.length > 1) {
console.log('welcome to our new focus... myself'); console.log('welcome to our new focus... myself');
focus = new ColibriFocus(connection, config.hosts.bridge); focus = new ColibriFocus(connection, config.hosts.bridge);
if (Object.keys(connection.emuc.members).length > 0) { if (Object.keys(connection.emuc.members).length > 0) {
@ -603,7 +625,8 @@ $(document).bind('left.muc', function (event, jid) {
focus = new ColibriFocus(connection, config.hosts.bridge); focus = new ColibriFocus(connection, config.hosts.bridge);
} }
if (connection.emuc.getPrezi(jid)) { if (connection.emuc.getPrezi(jid)) {
$(document).trigger('presentationremoved.muc', [jid, connection.emuc.getPrezi(jid)]); $(document).trigger('presentationremoved.muc',
[jid, connection.emuc.getPrezi(jid)]);
} }
}); });
@ -686,6 +709,11 @@ $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
} }
if (focus) {
mutedAudios[jid] = isMuted;
updateRemoteVideoMenu(jid, isMuted);
}
if (videoSpanId) if (videoSpanId)
showAudioIndicator(videoSpanId, isMuted); showAudioIndicator(videoSpanId, isMuted);
}); });
@ -700,7 +728,7 @@ $(document).bind('videomuted.muc', function (event, jid, isMuted) {
} }
if (videoSpanId) if (videoSpanId)
showAudioIndicator(videoSpanId, isMuted); showVideoIndicator(videoSpanId, isMuted);
}); });
/** /**
@ -821,8 +849,11 @@ function toggleVideo() {
* Mutes / unmutes audio for the local participant. * Mutes / unmutes audio for the local participant.
*/ */
function toggleAudio() { function toggleAudio() {
if (!(connection && connection.jingle.localAudio)) if (!(connection && connection.jingle.localAudio)) {
preMuted = true;
return; return;
}
var localAudio = connection.jingle.localAudio; var localAudio = connection.jingle.localAudio;
for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) { for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
var audioEnabled = localAudio.getAudioTracks()[idx].enabled; var audioEnabled = localAudio.getAudioTracks()[idx].enabled;
@ -831,6 +862,8 @@ function toggleAudio() {
connection.emuc.addAudioInfoToPresence(audioEnabled); //isMuted is the opposite of audioEnabled connection.emuc.addAudioInfoToPresence(audioEnabled); //isMuted is the opposite of audioEnabled
connection.emuc.sendPresence(); connection.emuc.sendPresence();
} }
buttonClick("#mute", "icon-microphone icon-mic-disabled");
} }
/** /**
@ -1167,6 +1200,22 @@ function buttonClick(id, classname) {
$(id).toggleClass(classname); // add the class to the clicked element $(id).toggleClass(classname); // add the class to the clicked element
} }
/**
* Shows a message to the user.
*
* @param titleString the title of the message
* @param messageString the text of the message
*/
function openMessageDialog(titleString, messageString) {
console.log("OPEN MESSAGE DIALOG");
$.prompt(messageString,
{
title: titleString,
persistent: false
}
);
}
/** /**
* Opens the lock room dialog. * Opens the lock room dialog.
*/ */
@ -1448,7 +1497,7 @@ function ensurePeerContainerExists(peerJid) {
return; return;
} }
var container = addRemoteVideoContainer(videoSpanId); var container = addRemoteVideoContainer(peerJid, videoSpanId);
var nickfield = document.createElement('span'); var nickfield = document.createElement('span');
nickfield.className = "nick"; nickfield.className = "nick";
@ -1457,11 +1506,15 @@ function ensurePeerContainerExists(peerJid) {
resizeThumbnails(); resizeThumbnails();
} }
function addRemoteVideoContainer(id) { function addRemoteVideoContainer(peerJid, spanId) {
var container = document.createElement('span'); var container = document.createElement('span');
container.id = id; container.id = spanId;
container.className = 'videocontainer'; container.className = 'videocontainer';
var remotes = document.getElementById('remoteVideos'); var remotes = document.getElementById('remoteVideos');
if (focus)
addRemoteVideoMenu(peerJid, container);
remotes.appendChild(container); remotes.appendChild(container);
return container; return container;
} }
@ -1476,6 +1529,97 @@ function createFocusIndicatorElement(parentElement) {
parentElement.appendChild(focusIndicator); parentElement.appendChild(focusIndicator);
} }
function addRemoteVideoMenu(jid, parentElement) {
var spanElement = document.createElement('span');
spanElement.className = 'remotevideomenu';
parentElement.appendChild(spanElement);
var menuElement = document.createElement('i');
menuElement.className = 'fa fa-angle-down';
menuElement.title = 'Remote user controls';
spanElement.appendChild(menuElement);
// <ul class="popupmenu">
// <li><a href="#">Mute</a></li>
// <li><a href="#">Eject</a></li>
// </ul>
var popupmenuElement = document.createElement('ul');
popupmenuElement.className = 'popupmenu';
popupmenuElement.id = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
spanElement.appendChild(popupmenuElement);
var muteMenuItem = document.createElement('li');
var muteLinkItem = document.createElement('a');
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
if (!mutedAudios[jid]) {
muteLinkItem.innerHTML = mutedIndicator + 'Mute';
muteLinkItem.className = 'mutelink';
}
else {
muteLinkItem.innerHTML = mutedIndicator + ' Muted';
muteLinkItem.className = 'mutelink disabled';
}
muteLinkItem.onclick = function(){
if ($(this).attr('disabled') != undefined) {
event.preventDefault();
}
var isMute = !mutedAudios[jid];
connection.moderate.setMute(jid, isMute);
popupmenuElement.setAttribute('style', 'display:none;');
if (isMute) {
this.innerHTML = mutedIndicator + ' Muted';
this.className = 'mutelink disabled';
}
else {
this.innerHTML = mutedIndicator + ' Mute';
this.className = 'mutelink';
}
};
muteMenuItem.appendChild(muteLinkItem);
popupmenuElement.appendChild(muteMenuItem);
var ejectIndicator = "<i class='fa fa-eject'></i>";
var ejectMenuItem = document.createElement('li');
var ejectLinkItem = document.createElement('a');
ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
ejectLinkItem.onclick = function(){
connection.moderate.eject(jid);
popupmenuElement.setAttribute('style', 'display:none;');
};
ejectMenuItem.appendChild(ejectLinkItem);
popupmenuElement.appendChild(ejectMenuItem);
}
function updateRemoteVideoMenu(jid, isMuted) {
var muteMenuItem
= $('#remote_popupmenu_'
+ Strophe.getResourceFromJid(jid)
+ '>li>a.mutelink');
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
if (muteMenuItem.length) {
var muteLink = muteMenuItem.get(0);
if (isMuted === 'true') {
muteLink.innerHTML = mutedIndicator + ' Muted';
muteLink.className = 'mutelink disabled';
}
else {
muteLink.innerHTML = mutedIndicator + ' Mute';
muteLink.className = 'mutelink';
}
}
}
/** /**
* Toggles the application in and out of full screen mode * Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome). * (a.k.a. presentation mode in Chrome).

View File

@ -205,7 +205,6 @@ button {
} }
button, input, select, textarea { button, input, select, textarea {
font-size: 100%;
margin: 0; margin: 0;
vertical-align: baseline; vertical-align: baseline;
} }

56
css/popup_menu.css Normal file
View File

@ -0,0 +1,56 @@
/*Initialize*/
ul.popupmenu {
display:none;
position: absolute;
padding:0;
margin: 0;
bottom: 20px;
padding-bottom: 5px;
padding-top: 5px;
right: 10px;
left: 0px;
width: 100px;
background-color: rgba(0,0,0,1);
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
border-radius:8px;
}
ul.popupmenu:after {
content: url('../images/popupPointer.png');
display: block;
position: absolute;
bottom: -9px;
left: 13px;
}
ul.popupmenu li {
list-style-type: none;
text-align: left;
}
ul.popupmenu li:hover {
background-color: rgba(82, 82, 82, .7);
border-radius:2px;
}
/*Link Appearance*/
ul.popupmenu li a {
text-decoration: none;
color: #fff;
padding: 5px;
display: inline-block;
font-size: 9pt;
}
ul.popupmenu li a i {
width: 12px;
}
span.remotevideomenu:hover ul.popupmenu {
display:block !important;
}
a.disabled {
color: gray !important;
pointer-events: none;
}

View File

@ -16,9 +16,8 @@
left: 0; left: 0;
right: 0; right: 0;
width:auto; width:auto;
overflow: hidden;
border:1px solid transparent; border:1px solid transparent;
z-index: 2; z-index: 5;
} }
.videocontainer { .videocontainer {
@ -41,8 +40,8 @@
content:""; content:"";
cursor: pointer; cursor: pointer;
cursor: hand; cursor: hand;
transform:scale(1.08, 1.08); /* transform:scale(1.08, 1.08);
-webkit-transform:scale(1.08, 1.08); -webkit-transform:scale(1.08, 1.08); */
transition-duration: 0.5s; transition-duration: 0.5s;
-webkit-transition-duration: 0.5s; -webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse; -webkit-animation-name: greyPulse;
@ -107,7 +106,8 @@
display: none; display: none;
} }
#remoteVideos .videocontainer>span.focusindicator { #remoteVideos .videocontainer>span.focusindicator,
#remoteVideos .videocontainer>span.remotevideomenu {
display: inline-block; display: inline-block;
position: absolute; position: absolute;
color: #FFFFFF; color: #FFFFFF;

BIN
images/popupPointer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -31,6 +31,7 @@
<script src="prezi.js?v=2"></script><!-- prezi plugin --> <script src="prezi.js?v=2"></script><!-- prezi plugin -->
<script src="smileys.js?v=1"></script><!-- smiley images --> <script src="smileys.js?v=1"></script><!-- smiley images -->
<script src="replacement.js?v=5"></script><!-- link and smiley replacement --> <script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
<script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
<script src="analytics.js?v=1"></script><!-- google analytics plugin --> <script src="analytics.js?v=1"></script><!-- google analytics plugin -->
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet"> <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="css/font.css"/> <link rel="stylesheet" href="css/font.css"/>
@ -38,6 +39,7 @@
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=4" id="videolayout_default"/> <link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=4" id="videolayout_default"/>
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4"> <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
<link rel="stylesheet" href="css/modaldialog.css?v=3"> <link rel="stylesheet" href="css/modaldialog.css?v=3">
<link rel="stylesheet" href="css/popup_menu.css?v=1">
<!-- <!--
Link used for inline installation of chrome desktop streaming extension, Link used for inline installation of chrome desktop streaming extension,
is updated automatically from the code with the value defined in config.js --> is updated automatically from the code with the value defined in config.js -->
@ -49,7 +51,7 @@
<body> <body>
<div id="header"> <div id="header">
<span id="toolbar"> <span id="toolbar">
<a class="button" onclick='buttonClick("#mute", "icon-microphone icon-mic-disabled");toggleAudio();'> <a class="button" onclick='toggleAudio();'>
<i id="mute" title="Mute / unmute" class="icon-microphone"></i></a> <i id="mute" title="Mute / unmute" class="icon-microphone"></i></a>
<div class="header_button_separator"></div> <div class="header_button_separator"></div>
<a class="button" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'> <a class="button" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>

View File

@ -848,3 +848,46 @@ ColibriFocus.prototype.terminate = function (session, reason) {
delete this.remotessrc[session.peerjid]; delete this.remotessrc[session.peerjid];
this.modifySources(); this.modifySources();
}; };
ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
var term = $iq({to: session.peerjid, type: 'set'})
.c('jingle',
{xmlns: 'urn:xmpp:jingle:1',
action: 'session-terminate',
initiator: session.me,
sid: session.sid})
.c('reason')
.c(reason || 'success');
if (text) {
term.up().c('text').t(text);
}
this.connection.sendIQ(term,
function () {
if (!session)
return;
if (session.peerconnection) {
session.peerconnection.close();
session.peerconnection = null;
}
session.terminate();
var ack = {};
ack.source = 'terminate';
$(document).trigger('ack.jingle', [session.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
$(document).trigger('ack.jingle', [self.sid, error]);
},
10000);
if (this.statsinterval !== null) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
};

View File

@ -90,5 +90,5 @@ ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
}; };
ColibriSession.prototype.sendTerminate = function (reason, text) { ColibriSession.prototype.sendTerminate = function (reason, text) {
console.log('ColibriSession.sendTerminate'); this.colibri.sendTerminate(this, reason, text);
}; };

View File

@ -112,17 +112,27 @@ Strophe.addConnectionPlugin('jingle', {
$(document).trigger('callaccepted.jingle', [sess.sid]); $(document).trigger('callaccepted.jingle', [sess.sid]);
break; break;
case 'session-terminate': case 'session-terminate':
console.log('terminating...'); // If this is not the focus sending the terminate, we have
// nothing more to do here.
if (Object.keys(this.sessions).length < 1
|| !(this.sessions[Object.keys(this.sessions)[0]]
instanceof JingleSession))
{
break;
}
console.log('terminating...', sess.sid);
sess.terminate(); sess.terminate();
this.terminate(sess.sid); this.terminate(sess.sid);
if ($(iq).find('>jingle>reason').length) { if ($(iq).find('>jingle>reason').length) {
$(document).trigger('callterminated.jingle', [ $(document).trigger('callterminated.jingle', [
sess.sid, sess.sid,
sess.peerjid,
$(iq).find('>jingle>reason>:first')[0].tagName, $(iq).find('>jingle>reason>:first')[0].tagName,
$(iq).find('>jingle>reason>text').text() $(iq).find('>jingle>reason>text').text()
]); ]);
} else { } else {
$(document).trigger('callterminated.jingle', [sess.sid]); $(document).trigger('callterminated.jingle',
[sess.sid, sess.peerjid]);
} }
break; break;
case 'transport-info': case 'transport-info':
@ -192,6 +202,7 @@ Strophe.addConnectionPlugin('jingle', {
delete this.sessions[sid]; delete this.sessions[sid];
} }
}, },
// Used to terminate a session when an unavailable presence is received.
terminateByJid: function (jid) { terminateByJid: function (jid) {
if (this.jid2session.hasOwnProperty(jid)) { if (this.jid2session.hasOwnProperty(jid)) {
var sess = this.jid2session[jid]; var sess = this.jid2session[jid];
@ -200,7 +211,22 @@ Strophe.addConnectionPlugin('jingle', {
console.log('peer went away silently', jid); console.log('peer went away silently', jid);
delete this.sessions[sess.sid]; delete this.sessions[sess.sid];
delete this.jid2session[jid]; delete this.jid2session[jid];
$(document).trigger('callterminated.jingle', [sess.sid, 'gone']); $(document).trigger('callterminated.jingle',
[sess.sid, jid], 'gone');
}
}
},
terminateRemoteByJid: function (jid, reason) {
if (this.jid2session.hasOwnProperty(jid)) {
var sess = this.jid2session[jid];
if (sess) {
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
sess.terminate();
console.log('terminate peer with jid', sess.sid, jid);
delete this.sessions[sess.sid];
delete this.jid2session[jid];
$(document).trigger('callterminated.jingle',
[sess.sid, jid, 'kicked']);
} }
} }
}, },

50
moderatemuc.js Normal file
View File

@ -0,0 +1,50 @@
/**
* Moderate connection plugin.
*/
Strophe.addConnectionPlugin('moderate', {
connection: null,
roomjid: null,
myroomjid: null,
members: {},
list_members: [], // so we can elect a new focus
presMap: {},
preziMap: {},
joined: false,
isOwner: false,
init: function (conn) {
this.connection = conn;
this.connection.addHandler( this.onMute.bind(this),
'http://jitsi.org/jitmeet/audio',
'iq',
'set',
null,
null);
},
setMute: function(jid, mute) {
var iq = $iq({to: jid, type: 'set'})
.c('mute', {xmlns: 'http://jitsi.org/jitmeet/audio'})
.t(mute.toString())
.up();
this.connection.sendIQ(
iq,
function (result) {
console.log('set mute', result);
},
function (error) {
console.log('set mute error', error);
});
},
onMute: function(iq) {
var mute = $(iq).find('mute');
if (mute.length) {
toggleAudio();
}
return true;
},
eject: function(jid) {
connection.jingle.terminateRemoteByJid(jid, 'kick');
connection.emuc.kick(jid);
}
});

35
muc.js
View File

@ -36,6 +36,12 @@ Strophe.addConnectionPlugin('emuc', {
} }
this.sendPresence(); this.sendPresence();
}, },
doLeave: function() {
console.log("DO LEAVE", this.myroomjid);
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
this.presMap.length = 0;
this.connection.send(pres);
},
onPresence: function (pres) { onPresence: function (pres) {
var from = pres.getAttribute('from'); var from = pres.getAttribute('from');
var type = pres.getAttribute('type'); var type = pres.getAttribute('type');
@ -125,9 +131,22 @@ Strophe.addConnectionPlugin('emuc', {
}, },
onPresenceUnavailable: function (pres) { onPresenceUnavailable: function (pres) {
var from = pres.getAttribute('from'); var from = pres.getAttribute('from');
// Status code 110 indicates that this notification is "self-presence".
if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
delete this.members[from]; delete this.members[from];
this.list_members.splice(this.list_members.indexOf(from), 1); this.list_members.splice(this.list_members.indexOf(from), 1);
$(document).trigger('left.muc', [from]); $(document).trigger('left.muc', [from]);
}
// If the status code is 110 this means we're leaving and we would like
// to remove everyone else from our view, so we trigger the event.
else if (this.list_members.length > 1) {
for (var i = 0; i < this.list_members.length; i++) {
var member = this.list_members[i];
delete this.members[i];
this.list_members.splice(i, 1);
$(document).trigger('left.muc', member);
}
}
return true; return true;
}, },
onPresenceError: function (pres) { onPresenceError: function (pres) {
@ -188,6 +207,21 @@ Strophe.addConnectionPlugin('emuc', {
} }
); );
}, },
kick: function (jid) {
var kickIQ = $iq({to: this.roomjid, type: 'set'})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
.c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
.c('reason').t('You have been kicked.').up().up().up();
this.connection.sendIQ(
kickIQ,
function (result) {
console.log('Kick participant with jid: ' + jid, result);
},
function (error) {
console.log('Kick participant error: ', error);
});
},
sendPresence: function () { sendPresence: function () {
var pres = $pres({to: this.presMap['to'] }); var pres = $pres({to: this.presMap['to'] });
pres.c('x', {xmlns: this.presMap['xns']}); pres.c('x', {xmlns: this.presMap['xns']});
@ -210,7 +244,6 @@ Strophe.addConnectionPlugin('emuc', {
} }
if (this.presMap['videons']) { if (this.presMap['videons']) {
console.log("SEND VIDEO MUTED", this.presMap['videomuted']);
pres.c('videomuted', {xmlns: this.presMap['videons']}) pres.c('videomuted', {xmlns: this.presMap['videons']})
.t(this.presMap['videomuted']).up(); .t(this.presMap['videomuted']).up();
} }