Merge branch 'ssfocus'

This commit is contained in:
paweldomas 2014-12-04 11:19:45 +01:00
commit 4776605dec
21 changed files with 4460 additions and 270 deletions

236
app.js
View File

@ -2,12 +2,11 @@
/* application specific logic */
var connection = null;
var authenticatedUser = false;
var focus = null;
var activecall = null;
var RTC = null;
var nickname = null;
var sharedKey = '';
var recordingToken ='';
var focusJid = null;
var roomUrl = null;
var roomName = null;
var ssrc2jid = {};
@ -41,6 +40,16 @@ var ssrc2videoType = {};
*/
var focusedVideoSrc = null;
var mutedAudios = {};
/**
* Remembers if we were muted by the focus.
* @type {boolean}
*/
var forceMuted = false;
/**
* Indicates if we have muted our audio before the conference has started.
* @type {boolean}
*/
var preMuted = false;
var localVideoSrc = null;
var flipXLocalVideo = true;
@ -219,11 +228,9 @@ function maybeDoJoin() {
}
}
function doJoin() {
function generateRoomName() {
var roomnode = null;
var path = window.location.pathname;
var roomjid;
// determinde the room node from the url
// TODO: just the roomnode or the whole bare jid?
@ -251,7 +258,20 @@ function doJoin() {
}
roomName = roomnode + '@' + config.hosts.muc;
}
function doJoin() {
if (!roomName) {
generateRoomName();
}
Moderator.allocateConferenceFocus(
roomName, doJoinAfterFocus);
}
function doJoinAfterFocus() {
var roomjid;
roomjid = roomName;
if (config.useNicks) {
@ -410,7 +430,10 @@ function waitForPresence(data, sid) {
container = document.getElementById(
'participant_' + Strophe.getResourceFromJid(data.peerjid));
} else {
if (data.stream.id !== 'mixedmslabel') {
if (data.stream.id !== 'mixedmslabel'
// FIXME: default stream is added always with new focus
// (to be investigated)
&& data.stream.id !== 'default') {
console.error('can not associate stream',
data.stream.id,
'with a participant');
@ -638,16 +661,6 @@ $(document).bind('conferenceCreated.jingle', function (event, focus)
}
});
$(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
// Leave the room if my call has been remotely terminated.
if (connection.emuc.joined && focus == null && reason === 'kick') {
sessionTerminated = true;
connection.emuc.doLeave();
messageHandler.openMessageDialog("Session Terminated",
"Ouch! You have been kicked out of the meet!");
}
});
$(document).bind('setLocalDescription.jingle', function (event, sid) {
// put our ssrcs into presence so other clients can identify our stream
var sess = connection.jingle.sessions[sid];
@ -736,27 +749,6 @@ $(document).bind('joined.muc', function (event, jid, info) {
document.createTextNode(Strophe.getResourceFromJid(jid) + ' (me)')
);
if (Object.keys(connection.emuc.members).length < 1) {
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
Toolbar.showRecordingButton(false);
}
if (!focus)
{
Toolbar.showSipCallButton(false);
}
if (focus && config.etherpad_base) {
Etherpad.init();
}
VideoLayout.showFocusIndicator();
// Add myself to the contact list.
ContactList.addContact(jid, SettingsMenu.getEmail() || SettingsMenu.getUID());
@ -777,7 +769,12 @@ $(document).bind('entered.muc', function (event, jid, info, pres) {
'connected',
'connected');
console.log('is focus? ' + (focus ? 'true' : 'false'));
if (info.isFocus)
{
focusJid = jid;
console.info("Ignore focus: " + jid +", real JID: " + info.jid);
return;
}
// Add Peer's container
var id = $(pres).find('>userID').text();
@ -792,7 +789,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) {
APIConnector.triggerEvent("participantJoined",{jid: jid});
}
if (focus !== null) {
/*if (focus !== null) {
// FIXME: this should prepare the video
if (focus.confid === null) {
console.log('make new conference with', jid);
@ -807,7 +804,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) {
console.log('invite', jid, 'into conference');
focus.addNewParticipant(jid);
}
}
}*/
});
$(document).bind('left.muc', function (event, jid) {
@ -848,41 +845,6 @@ $(document).bind('left.muc', function (event, jid) {
connection.jingle.terminateByJid(jid);
if (focus == null
// I shouldn't be the one that left to enter here.
&& jid !== connection.emuc.myroomjid
&& connection.emuc.myroomjid === connection.emuc.list_members[0]
// If our session has been terminated for some reason
// (kicked, hangup), don't try to become the focus
&& !sessionTerminated) {
console.log('welcome to our new focus... myself');
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
if (Object.keys(connection.emuc.members).length > 0) {
focus.makeConference(Object.keys(connection.emuc.members));
Toolbar.showRecordingButton(true);
}
$(document).trigger('focusechanged.muc', [focus]);
}
else if (focus && Object.keys(connection.emuc.members).length === 0) {
console.log('everyone left');
// FIXME: closing the connection is a hack to avoid some
// problems with reinit
disposeConference();
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
Toolbar.showRecordingButton(false);
}
if (connection.emuc.getPrezi(jid)) {
$(document).trigger('presentationremoved.muc',
[jid, connection.emuc.getPrezi(jid)]);
@ -929,12 +891,16 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
if (displayName && displayName.length > 0)
$(document).trigger('displaynamechanged',
[jid, displayName]);
if (focus !== null && info.displayName !== null) {
focus.setEndpointDisplayName(jid, info.displayName);
[jid, info.displayName]);
if (info.isFocus)
{
return;
}
/*if (focus !== null && info.displayName !== null) {
focus.setEndpointDisplayName(jid, info.displayName);
}*/
//check if the video bridge is available
if($(pres).find(">bridgeIsDown").length > 0 && !bridgeIsDown) {
bridgeIsDown = true;
@ -958,6 +924,17 @@ $(document).bind('presence.status.muc', function (event, jid, info, pres) {
});
$(document).bind('kicked.muc', function (event, jid) {
console.info(jid + " has been kicked from MUC!");
if (connection.emuc.myroomjid === jid) {
sessionTerminated = true;
disposeConference(false);
connection.emuc.doLeave();
messageHandler.openMessageDialog("Session Terminated",
"Ouch! You have been kicked out of the meet!");
}
});
$(document).bind('passwordrequired.muc', function (event, jid) {
console.log('on password required', jid);
@ -1046,7 +1023,7 @@ function isVideoSrcDesktop(jid) {
}
function getConferenceHandler() {
return focus ? focus : activecall;
return activecall;
}
function toggleVideo() {
@ -1076,28 +1053,45 @@ function toggleVideo() {
* Mutes / unmutes audio for the local participant.
*/
function toggleAudio() {
setAudioMuted(!isAudioMuted());
}
/**
* Sets muted audio state for the local participant.
*/
function setAudioMuted(mute) {
if (!(connection && connection.jingle.localAudio)) {
preMuted = mute;
// We still click the button.
buttonClick("#mute", "icon-microphone icon-mic-disabled");
return;
}
if (forceMuted && !mute) {
console.info("Asking focus for unmute");
connection.moderate.setMute(connection.emuc.myroomjid, mute);
// FIXME: wait for result before resetting muted status
forceMuted = false;
}
if (mute == isAudioMuted()) {
// Nothing to do
return;
}
// It is not clear what is the right way to handle multiple tracks.
// So at least make sure that they are all muted or all unmuted and
// that we send presence just once.
var localAudioTracks = connection.jingle.localAudio.getAudioTracks();
if (localAudioTracks.length > 0) {
var audioEnabled = localAudioTracks[0].enabled;
for (var idx = 0; idx < localAudioTracks.length; idx++) {
localAudioTracks[idx].enabled = !audioEnabled;
localAudioTracks[idx].enabled = !mute;
}
// isMuted is the opposite of audioEnabled
connection.emuc.addAudioInfoToPresence(audioEnabled);
connection.emuc.sendPresence();
VideoLayout.showLocalAudioIndicator(audioEnabled);
}
// isMuted is the opposite of audioEnabled
connection.emuc.addAudioInfoToPresence(mute);
connection.emuc.sendPresence();
VideoLayout.showLocalAudioIndicator(mute);
buttonClick("#mute", "icon-microphone icon-mic-disabled");
}
@ -1118,51 +1112,7 @@ function isAudioMuted()
// Starts or stops the recording for the conference.
function toggleRecording() {
if (focus === null || focus.confid === null) {
console.log('non-focus, or conference not yet organized: not enabling recording');
return;
}
if (!recordingToken)
{
messageHandler.openTwoButtonDialog(null,
'<h2>Enter recording token</h2>' +
'<input id="recordingToken" type="text" placeholder="token" autofocus>',
false,
"Save",
function (e, v, m, f) {
if (v) {
var token = document.getElementById('recordingToken');
if (token.value) {
setRecordingToken(Util.escapeHtml(token.value));
toggleRecording();
}
}
},
function (event) {
document.getElementById('recordingToken').focus();
}
);
return;
}
var oldState = focus.recordingEnabled;
Toolbar.toggleRecordingButtonState();
focus.setRecording(!oldState,
recordingToken,
function (state) {
console.log("New recording state: ", state);
if (state == oldState) //failed to change, reset the token because it might have been wrong
{
Toolbar.toggleRecordingButtonState();
setRecordingToken(null);
}
}
);
Recording.toggleRecording();
}
/**
@ -1380,6 +1330,8 @@ $(document).ready(function () {
}
});
Moderator.init();
// Set the defaults for prompt dialogs.
jQuery.prompt.setDefaults({persistent: false});
@ -1495,7 +1447,6 @@ function disposeConference(onUnload) {
if(onUnload) {
stopLocalRtpStatsCollector();
}
focus = null;
activecall = null;
}
@ -1562,10 +1513,6 @@ function setSharedKey(sKey) {
sharedKey = sKey;
}
function setRecordingToken(token) {
recordingToken = token;
}
/**
* Updates the room invite url.
*/
@ -1585,11 +1532,13 @@ function updateRoomUrl(newRoomUrl) {
* Warning to the user that the conference window is about to be closed.
*/
function closePageWarning() {
/*
FIXME: do we need a warning when the focus is a server-side one now ?
if (focus !== null)
return "You are the owner of this conference call and"
+ " you are about to end it.";
else
return "You are about to leave this conversation.";
else*/
return "You are about to leave this conversation.";
}
/**
@ -1626,6 +1575,13 @@ function setView(viewName) {
// }
}
$(document).bind('error.jingle',
function (event, session, error)
{
console.error("Jingle error", error);
}
);
$(document).bind('fatalError.jingle',
function (event, session, error)
{

View File

@ -4,7 +4,8 @@ var config = {
//anonymousdomain: 'guest.example.com',
muc: 'conference.jitsi-meet.example.com', // FIXME: use XEP-0030
bridge: 'jitsi-videobridge.jitsi-meet.example.com', // FIXME: use XEP-0030
//call_control: 'callcontrol.jitsi-meet.example.com'
//call_control: 'callcontrol.jitsi-meet.example.com',
focus: 'focus.jitsi-meet.example.com'
},
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
// useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
@ -12,6 +13,7 @@ var config = {
useNicks: false,
bosh: '//jitsi-meet.example.com/http-bind', // FIXME: use xep-0156 for that
clientNode: 'http://jitsi.org/jitsimeet', // The name of client node advertised in XEP-0115 'c' stanza
focusUserJid: 'focus@auth.jitsi-meet.example.com', // The real JID of focus participant
//defaultSipNumber: '', // Default SIP number
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
@ -27,6 +29,7 @@ var config = {
enableRecording: false,
enableWelcomePage: true,
enableSimulcast: false,
enableFirefoxSupport: false //firefox support is still experimental, only one-to-one conferences with chrome focus
enableFirefoxSupport: false, //firefox support is still experimental, only one-to-one conferences with chrome focus
// will work when simulcast, bundle, mux, lastN and SCTP are disabled.
logStats: false // Enable logging of PeerConnection stats via the focus
};

View File

@ -23,6 +23,8 @@ case "$1" in
. /etc/jitsi/videobridge/config
. /etc/jitsi/jicofo/config
# loading debconf
. /usr/share/debconf/confmodule
@ -41,9 +43,25 @@ case "$1" in
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
fi
# create 'focus@auth.domain' prosody user
# FIXME this duplicates with below
prosodyctl register $JICOFO_AUTH_USER $JICOFO_AUTH_DOMAIN $JICOFO_AUTH_PASSWORD
fi
# on UPGRADE to server side focus check if focus is configured
if [ -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"auth.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG; then
echo -e "\nVirtualHost \"auth.$JVB_HOSTNAME\"" >> $PROSODY_HOST_CONFIG
echo -e " authentication = \"internal_plain\"\n" >> $PROSODY_HOST_CONFIG
echo -e "admins = { \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\" }\n" >> $PROSODY_HOST_CONFIG
echo -e "Component \"focus.$JVB_HOSTNAME\"" >> $PROSODY_HOST_CONFIG
echo -e " component_secret=\"$JICOFO_SECRET\"\n" >> $PROSODY_HOST_CONFIG
# create 'focus@auth.domain' prosody user
# FIXME this duplicates with above
prosodyctl register $JICOFO_AUTH_USER $JICOFO_AUTH_DOMAIN $JICOFO_AUTH_PASSWORD
fi
if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
@ -60,6 +78,7 @@ case "$1" in
if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
invoke-rc.d prosody restart
invoke-rc.d jitsi-videobridge restart
invoke-rc.d jicofo restart
fi
;;

View File

@ -1,4 +1,4 @@
/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */
/* global $, alert, changeLocalVideo, chrome, config, connection, getConferenceHandler, getUserMediaWithConstraints, VideoLayout */
/**
* Indicates that desktop stream is currently in use(for toggle purpose).
* @type {boolean}
@ -283,9 +283,9 @@ function toggleScreenSharing() {
}
switchInProgress = true;
// Only the focus is able to set a shared key.
if (!isUsingScreenStream)
{
// Switch to desktop stream
obtainDesktopStream(
function (stream) {
// We now use screen stream

View File

@ -19,3 +19,11 @@ Component "conference.jitmeet.example.com" "muc"
Component "jitsi-videobridge.jitmeet.example.com"
component_secret = "jitmeetSecret"
VirtualHost "auth.jitmeet.example.com"
authentication = "internal_plain"
admins = { "focusUser@auth.jitmeet.example.com" }
Component "focus.jitmeet.example.com"
component_secret = "focusSecret"

View File

@ -1,4 +1,5 @@
/* global $, config, Prezi, Util, connection, setLargeVideoVisible, dockToolbar */
/* global $, config, connection, dockToolbar, Moderator, Prezi,
setLargeVideoVisible, ToolbarToggler, Util, VideoLayout */
var Etherpad = (function (my) {
var etherpadName = null;
var etherpadIFrame = null;
@ -161,7 +162,7 @@ var Etherpad = (function (my) {
*/
$(document).bind('etherpadadded.muc', function (event, jid, etherpadName) {
console.log("Etherpad added", etherpadName);
if (config.etherpad_base && !focus) {
if (config.etherpad_base && !Moderator.isModerator()) {
Etherpad.init(etherpadName);
}
});
@ -169,6 +170,7 @@ var Etherpad = (function (my) {
/**
* On focus changed event.
*/
// FIXME: there is no such event as 'focusechanged.muc'
$(document).bind('focusechanged.muc', function (event, focus) {
console.log("Focus changed");
if (config.etherpad_base)

View File

@ -28,6 +28,7 @@
<script src="libs/rayo.js?v=1"></script>
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
<script src="libs/pako.bundle.js?v=1"></script><!-- zlib deflate -->
<script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
<script src="interface_config.js?v=4"></script>
<script src="muc.js?v=17"></script><!-- simple MUC library -->
@ -56,8 +57,10 @@
<script src="audio_levels.js?v=2"></script><!-- audio levels plugin -->
<script src="media_stream.js?v=2"></script><!-- media stream -->
<script src="bottom_toolbar.js?v=6"></script><!-- media stream -->
<script src="moderator.js?v=1"></script><!-- media stream -->
<script src="roomname_generator.js?v=1"></script><!-- generator for random room names -->
<script src="keyboard_shortcut.js?v=3"></script>
<script src="recording.js?v=1"></script>
<script src="tracking.js?v=1"></script><!-- tracking -->
<script src="jitsipopover.js?v=3"></script>
<script src="message_handler.js?v=2"></script>

3747
libs/pako.bundle.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -372,6 +372,15 @@ TraceablePeerConnection.prototype.modifySources = function(successCallback) {
});
this.removessrc = [];
// FIXME:
// this was a hack for the situation when only one peer exists
// in the conference.
// check if still required and remove
if (sdp.media[0])
sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv');
if (sdp.media[1])
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
sdp.raw = sdp.session + sdp.media.join('');
this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
function() {

View File

@ -30,8 +30,12 @@ Strophe.addConnectionPlugin('jingle', {
// this is dealt with by SDP O/A so we don't need to annouce this
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
//this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
if (config.useRtcpMux) {
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
}
if (config.useBundle) {
this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
}
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
}
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);

View File

@ -131,22 +131,6 @@ JingleSession.prototype.accept = function () {
responder: this.responder,
sid: this.sid });
prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder', this.localStreamsSSRC);
this.connection.sendIQ(accept,
function () {
var ack = {};
ack.source = 'answer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName,
}:{};
error.source = 'answer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
var sdp = this.peerconnection.localDescription.sdp;
while (SDPUtil.find_line(sdp, 'a=inactive')) {
// FIXME: change any inactive to sendrecv or whatever they were originally
@ -156,6 +140,22 @@ JingleSession.prototype.accept = function () {
function () {
//console.log('setLocalDescription success');
$(document).trigger('setLocalDescription.jingle', [self.sid]);
this.connection.sendIQ(accept,
function () {
var ack = {};
ack.source = 'answer';
$(document).trigger('ack.jingle', [self.sid, ack]);
},
function (stanza) {
var error = ($(stanza).find('error').length) ? {
code: $(stanza).find('error').attr('code'),
reason: $(stanza).find('error :first')[0].tagName
}:{};
error.source = 'answer';
$(document).trigger('error.jingle', [self.sid, error]);
},
10000);
},
function (e) {
console.error('setLocalDescription failed', e);

View File

@ -105,11 +105,11 @@ SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_c
self.modifySources(function() {
console.log('modify sources done');
success_callback();
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
console.log("SDPs", oldSdp, newSdp);
self.notifyMySSRCUpdate(oldSdp, newSdp);
success_callback();
});
};

View File

@ -1,53 +1,60 @@
/* global $, $iq, config, connection, focusJid, forceMuted, messageHandler,
setAudioMuted, Strophe, toggleAudio */
/**
* 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);
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();
setMute: function (jid, mute) {
console.info("set mute", mute);
var iqToFocus = $iq({to: focusJid, type: 'set'})
.c('mute', {
xmlns: 'http://jitsi.org/jitmeet/audio',
jid: jid
})
.t(mute.toString())
.up();
this.connection.sendIQ(
iq,
function (result) {
console.log('set mute', result);
},
function (error) {
console.log('set mute error', error);
messageHandler.openReportDialog(null, 'Failed to mute ' +
$("#participant_" + jid).find(".displayname").text() ||
"participant" + '.', error);
});
iqToFocus,
function (result) {
console.log('set mute', result);
},
function (error) {
console.log('set mute error', error);
// FIXME: this causes an exception
//messageHandler.openReportDialog(null, 'Failed to mute ' +
// $("#participant_" + jid).find(".displayname").text() ||
//"participant" + '.', error);
});
},
onMute: function(iq) {
onMute: function (iq) {
var from = iq.getAttribute('from');
if (from !== focusJid) {
console.warn("Ignored mute from non focus peer");
return false;
}
var mute = $(iq).find('mute');
if (mute.length) {
toggleAudio();
var doMuteAudio = mute.text() === "true";
setAudioMuted(doMuteAudio);
forceMuted = doMuteAudio;
}
return true;
},
eject: function(jid) {
connection.jingle.terminateRemoteByJid(jid, 'kick');
eject: function (jid) {
// We're not the focus, so can't terminate
//connection.jingle.terminateRemoteByJid(jid, 'kick');
connection.emuc.kick(jid);
}
});

141
moderator.js Normal file
View File

@ -0,0 +1,141 @@
/* global $, $iq, config, connection, Etherpad, hangUp, roomName, Strophe,
Toolbar, Util, VideoLayout */
/**
* Contains logic responsible for enabling/disabling functionality available
* only to moderator users.
*/
var Moderator = (function (my) {
var getNextTimeout = Util.createExpBackoffTimer(1000);
var getNextErrorTimeout = Util.createExpBackoffTimer(1000);
my.isModerator = function () {
return connection.emuc.isModerator();
};
my.onModeratorStatusChanged = function (isModerator) {
Toolbar.showSipCallButton(isModerator);
Toolbar.showRecordingButton(
isModerator); //&&
// FIXME:
// Recording visible if
// there are at least 2(+ 1 focus) participants
//Object.keys(connection.emuc.members).length >= 3);
if (isModerator && config.etherpad_base) {
Etherpad.init();
}
$(document).trigger('local.role.moderator', [isModerator]);
};
my.init = function () {
$(document).bind(
'role.changed.muc',
function (event, jid, info, pres) {
console.info(
"Role changed for " + jid + ", new role: " + info.role);
VideoLayout.showModeratorIndicator();
}
);
$(document).bind(
'local.role.changed.muc',
function (event, jid, info, pres) {
console.info("My role changed, new role: " + info.role);
VideoLayout.showModeratorIndicator();
Moderator.onModeratorStatusChanged(Moderator.isModerator());
}
);
$(document).bind(
'left.muc',
function (event, jid) {
console.info("Someone left is it focus ? " + jid);
var resource = Strophe.getResourceFromJid(jid);
if (resource === 'focus') {
console.info(
"Focus has left the room - leaving conference");
//hangUp();
// We'd rather reload to have everything re-initialized
// FIXME: show some message before reload
location.reload();
}
}
);
};
my.createConferenceIq = function () {
var elem = $iq({to: config.hosts.focus, type: 'set'});
elem.c('conference', {
xmlns: 'http://jitsi.org/protocol/focus',
room: roomName
});
if (config.channelLastN !== undefined)
{
elem.c(
'property',
{ name: 'channelLastN', value: config.channelLastN})
.up();
}
if (config.adaptiveLastN !== undefined)
{
elem.c(
'property',
{ name: 'adaptiveLastN', value: config.adaptiveLastN})
.up();
}
if (config.adaptiveSimulcast !== undefined)
{
elem.c(
'property',
{ name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
.up();
}
elem.up();
return elem;
};
// FIXME: we need to show the fact that we're waiting for the focus
// to the user(or that focus is not available)
my.allocateConferenceFocus = function (roomName, callback) {
var iq = Moderator.createConferenceIq();
connection.sendIQ(
iq,
function (result) {
if ('true' === $(result).find('conference').attr('ready')) {
// Reset both timers
getNextTimeout(true);
getNextErrorTimeout(true);
callback();
} else {
var waitMs = getNextTimeout();
console.info("Waiting for the focus... " + waitMs);
// Reset error timeout
getNextErrorTimeout(true);
window.setTimeout(
function () {
Moderator.allocateConferenceFocus(
roomName, callback);
}, waitMs);
}
},
function (error) {
var waitMs = getNextErrorTimeout();
console.error("Focus error, retry after " + waitMs, error);
// Reset response timeout
getNextTimeout(true);
window.setTimeout(
function () {
Moderator.allocateConferenceFocus(roomName, callback);
}, waitMs);
}
);
};
return my;
}(Moderator || {}));

27
muc.js
View File

@ -12,6 +12,7 @@ Strophe.addConnectionPlugin('emuc', {
preziMap: {},
joined: false,
isOwner: false,
role: null,
init: function (conn) {
this.connection = conn;
},
@ -122,11 +123,22 @@ Strophe.addConnectionPlugin('emuc', {
member.affiliation = tmp.attr('affiliation');
member.role = tmp.attr('role');
// Focus recognition
member.jid = tmp.attr('jid');
member.isFocus = false;
if (member.jid && member.jid.indexOf(config.focusUserJid + "/") == 0) {
member.isFocus = true;
}
var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
member.displayName = (nicktag.length > 0 ? nicktag.text() : null);
if (from == this.myroomjid) {
if (member.affiliation == 'owner') this.isOwner = true;
if (this.role !== member.role) {
this.role = member.role;
$(document).trigger('local.role.changed.muc', [from, member, pres]);
}
if (!this.joined) {
this.joined = true;
$(document).trigger('joined.muc', [from, member]);
@ -137,9 +149,16 @@ Strophe.addConnectionPlugin('emuc', {
this.members[from] = member;
this.list_members.push(from);
$(document).trigger('entered.muc', [from, member, pres]);
} else {
// Presence update for existing participant
// Watch role change:
if (this.members[from].role != member.role) {
this.members[from].role = member.role
$(document).trigger('role.changed.muc', [from, member, pres]);
}
}
// Always trigger presence to update bindings
console.log('presence change from', from);
console.log('presence change from', from, pres);
$(document).trigger('presence.muc', [from, member, pres]);
// Trigger status message update
@ -167,6 +186,9 @@ Strophe.addConnectionPlugin('emuc', {
$(document).trigger('left.muc', member);
}
}
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
$(document).trigger('kicked.muc', [from]);
}
return true;
},
onPresenceError: function (pres) {
@ -470,5 +492,8 @@ Strophe.addConnectionPlugin('emuc', {
},
addUserIdToPresence: function(userId) {
this.presMap['userId'] = userId;
},
isModerator: function() {
return this.role === 'moderator';
}
});

108
recording.js Normal file
View File

@ -0,0 +1,108 @@
/* global $, $iq, config, connection, focusJid, messageHandler, Moderator,
Toolbar, Util */
var Recording = (function (my) {
var recordingToken = null;
var recordingEnabled;
my.setRecordingToken = function (token) {
recordingToken = token;
};
// Sends a COLIBRI message which enables or disables (according to 'state')
// the recording on the bridge. Waits for the result IQ and calls 'callback'
// with the new recording state, according to the IQ.
my.setRecording = function (state, token, callback) {
var self = this;
var elem = $iq({to: focusJid, type: 'set'});
elem.c('conference', {
xmlns: 'http://jitsi.org/protocol/colibri'
});
elem.c('recording', {state: state, token: token});
elem.up();
connection.sendIQ(elem,
function (result) {
console.log('Set recording "', state, '". Result:', result);
var recordingElem = $(result).find('>conference>recording');
var newState = ('true' === recordingElem.attr('state'));
recordingEnabled = newState;
callback(newState);
},
function (error) {
console.warn(error);
}
);
};
my.toggleRecording = function () {
if (!Moderator.isModerator()) {
console.log(
'non-focus, or conference not yet organized:' +
' not enabling recording');
return;
}
if (!recordingToken)
{
messageHandler.openTwoButtonDialog(null,
'<h2>Enter recording token</h2>' +
'<input id="recordingToken" type="text" placeholder="token" autofocus>',
false,
"Save",
function (e, v, m, f) {
if (v) {
var token = document.getElementById('recordingToken');
if (token.value) {
my.setRecordingToken(
Util.escapeHtml(token.value));
my.toggleRecording();
}
}
},
function (event) {
document.getElementById('recordingToken').focus();
}
);
return;
}
var oldState = recordingEnabled;
Toolbar.setRecordingButtonState(!oldState);
my.setRecording(!oldState,
recordingToken,
function (state) {
console.log("New recording state: ", state);
if (state === oldState)
{
// FIXME: new focus:
// this will not work when moderator changes
// during active session. Then it will assume that
// recording status has changed to true, but it might have
// been already true(and we only received actual status from
// the focus).
//
// SO we start with status null, so that it is initialized
// here and will fail only after second click, so if invalid
// token was used we have to press the button twice before
// current status will be fetched and token will be reset.
//
// Reliable way would be to return authentication error.
// Or status update when moderator connects.
// Or we have to stop recording session when current
// moderator leaves the room.
// Failed to change, reset the token because it might
// have been wrong
my.setRecordingToken(null);
}
// Update with returned status
Toolbar.setRecordingButtonState(state);
}
);
};
return my;
}(Recording || {}));

View File

@ -1,4 +1,5 @@
/* global ssrc2jid */
/* jshint -W117 */
/**
* Calculates packet lost percent using the number of lost packets and the
* number of all packet.
@ -133,6 +134,37 @@ function StatsCollector(peerconnection, audioLevelsInterval,
this.currentStatsReport = null;
this.baselineStatsReport = null;
this.audioLevelsIntervalId = null;
/**
* Gather PeerConnection stats once every this many milliseconds.
*/
this.GATHER_INTERVAL = 10000;
/**
* Log stats via the focus once every this many milliseconds.
*/
this.LOG_INTERVAL = 60000;
/**
* Gather stats and store them in this.statsToBeLogged.
*/
this.gatherStatsIntervalId = null;
/**
* Send the stats already saved in this.statsToBeLogged to be logged via
* the focus.
*/
this.logStatsIntervalId = null;
/**
* Stores the statistics which will be send to the focus to be logged.
*/
this.statsToBeLogged =
{
timestamps: [],
stats: {}
};
// Updates stats interval
this.audioLevelsIntervalMilis = audioLevelsInterval;
@ -156,6 +188,10 @@ StatsCollector.prototype.stop = function ()
this.audioLevelsIntervalId = null;
clearInterval(this.statsIntervalId);
this.statsIntervalId = null;
clearInterval(this.logStatsIntervalId);
this.logStatsIntervalId = null;
clearInterval(this.gatherStatsIntervalId);
this.gatherStatsIntervalId = null;
}
};
@ -238,8 +274,84 @@ StatsCollector.prototype.start = function ()
},
self.statsIntervalMilis
);
if (config.logStats) {
this.gatherStatsIntervalId = setInterval(
function () {
self.peerconnection.getStats(
function (report) {
self.addStatsToBeLogged(report.result());
},
function () {
}
);
},
this.GATHER_INTERVAL
);
this.logStatsIntervalId = setInterval(
function() { self.logStats(); },
this.LOG_INTERVAL);
}
};
/**
* Converts the stats to the format used for logging, and saves the data in
* this.statsToBeLogged.
* @param reports Reports as given by webkitRTCPerConnection.getStats.
*/
StatsCollector.prototype.addStatsToBeLogged = function (reports) {
var self = this;
var num_records = this.statsToBeLogged.timestamps.length;
this.statsToBeLogged.timestamps.push(new Date().getTime());
reports.map(function (report) {
var stat = self.statsToBeLogged.stats[report.id];
if (!stat) {
stat = self.statsToBeLogged.stats[report.id] = {};
}
stat.type = report.type;
report.names().map(function (name) {
var values = stat[name];
if (!values) {
values = stat[name] = [];
}
while (values.length < num_records) {
values.push(null);
}
values.push(report.stat(name));
});
});
};
StatsCollector.prototype.logStats = function () {
if (!focusJid) {
return;
}
var deflate = true;
var content = JSON.stringify(this.statsToBeLogged);
if (deflate) {
content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
}
content = Base64.encode(content);
// XEP-0337-ish
var message = $msg({to: focusJid, type: 'normal'});
message.c('log', { xmlns: 'urn:xmpp:eventlog',
id: 'PeerConnectionStats'});
message.c('message').t(content).up();
if (deflate) {
message.c('tag', {name: "deflated", value: "true"}).up();
}
message.up();
connection.send(message);
// Reset the stats
this.statsToBeLogged.stats = {};
this.statsToBeLogged.timestamps = [];
};
var keyMap = {
"firefox": {
"ssrc": "ssrc",

View File

@ -1,21 +1,23 @@
/* global $, buttonClick, config, lockRoom, messageHandler, Moderator, roomUrl,
setSharedKey, sharedKey, Util */
var Toolbar = (function (my) {
/**
* Disables and enables some of the buttons.
*/
my.setupButtonsFromConfig = function () {
if(config.disablePrezi)
if (config.disablePrezi)
{
$("#prezi_button").css({display: "none"});
}
}
};
/**
* Opens the lock room dialog.
*/
my.openLockDialog = function () {
// Only the focus is able to set a shared key.
if (focus === null) {
if (Moderator.isModerator()) {
if (sharedKey) {
messageHandler.openMessageDialog(null,
"This conversation is currently protected by" +
@ -73,7 +75,7 @@ var Toolbar = (function (my) {
*/
my.openLinkDialog = function () {
var inviteLink;
if (roomUrl == null) {
if (roomUrl === null) {
inviteLink = "Your conference is currently being created...";
} else {
inviteLink = encodeURI(roomUrl);
@ -106,7 +108,7 @@ var Toolbar = (function (my) {
* Invite participants to conference.
*/
function inviteParticipants() {
if (roomUrl == null)
if (roomUrl === null)
return;
var sharedKeyText = "";
@ -126,7 +128,8 @@ var Toolbar = (function (my) {
roomUrl +
"%0D%0A%0D%0A" +
sharedKeyText +
"Note that Jitsi Meet is currently only supported by Chromium," +
"Note that Jitsi Meet is currently" +
" only supported by Chromium," +
" Google Chrome and Opera, so you need" +
" to be using one of these browsers.%0D%0A%0D%0A" +
"Talk to you in a sec!";
@ -183,7 +186,7 @@ var Toolbar = (function (my) {
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
my.toggleFullScreen = function() {
my.toggleFullScreen = function () {
var fsElement = document.documentElement;
if (!document.mozFullScreen && !document.webkitIsFullScreen) {
@ -206,15 +209,15 @@ var Toolbar = (function (my) {
/**
* Unlocks the lock button state.
*/
my.unlockLockButton = function() {
if($("#lockIcon").hasClass("icon-security-locked"))
my.unlockLockButton = function () {
if ($("#lockIcon").hasClass("icon-security-locked"))
buttonClick("#lockIcon", "icon-security icon-security-locked");
};
/**
* Updates the lock button state to locked.
*/
my.lockLockButton = function() {
if($("#lockIcon").hasClass("icon-security"))
my.lockLockButton = function () {
if ($("#lockIcon").hasClass("icon-security"))
buttonClick("#lockIcon", "icon-security icon-security-locked");
};
@ -232,13 +235,19 @@ var Toolbar = (function (my) {
}
};
// Toggle the state of the recording button
my.toggleRecordingButtonState = function() {
$('#recordButton').toggleClass('active');
// Sets the state of the recording button
my.setRecordingButtonState = function (isRecording) {
if (isRecording) {
$('#recordButton').removeClass("icon-recEnable");
$('#recordButton').addClass("icon-recEnable active");
} else {
$('#recordButton').removeClass("icon-recEnable active");
$('#recordButton').addClass("icon-recEnable");
}
};
// Shows or hides SIP calls button
my.showSipCallButton = function(show){
my.showSipCallButton = function (show) {
if (config.hosts.call_control && show) {
$('#sipCallButton').css({display: "inline"});
} else {
@ -247,12 +256,13 @@ var Toolbar = (function (my) {
};
/**
* Sets the state of the button. The button has blue glow if desktop streaming is active.
* Sets the state of the button. The button has blue glow if desktop
* streaming is active.
* @param active the state of the desktop streaming.
*/
my.changeDesktopSharingButtonState = function (active) {
var button = $("#desktopsharing > a");
if(active)
if (active)
{
button.addClass("glow");
}
@ -260,7 +270,7 @@ var Toolbar = (function (my) {
{
button.removeClass("glow");
}
}
};
return my;
}(Toolbar || {}));

View File

@ -1,18 +1,20 @@
var ToolbarToggler = (function(my) {
/* global $, interfaceConfig, Moderator, showDesktopSharingButton */
var ToolbarToggler = (function (my) {
var toolbarTimeoutObject,
toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
/**
* Shows the main toolbar.
*/
my.showToolbar = function() {
my.showToolbar = function () {
var header = $("#header"),
bottomToolbar = $("#bottomToolbar");
if (!header.is(':visible') || !bottomToolbar.is(":visible")) {
header.show("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "+=40"}, 300);
if(!bottomToolbar.is(":visible")) {
bottomToolbar.show("slide", {direction: "right",duration: 300});
if (!bottomToolbar.is(":visible")) {
bottomToolbar.show(
"slide", {direction: "right", duration: 300});
}
if (toolbarTimeoutObject) {
@ -23,9 +25,10 @@ var ToolbarToggler = (function(my) {
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
}
if (focus != null)
if (Moderator.isModerator())
{
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// TODO: Enable settings functionality.
// Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"});
}
@ -46,8 +49,8 @@ var ToolbarToggler = (function(my) {
isToolbarHover = true;
}
});
if($("#bottomToolbar:hover").length > 0) {
isToolbarHover = true;
if ($("#bottomToolbar:hover").length > 0) {
isToolbarHover = true;
}
clearTimeout(toolbarTimeoutObject);
@ -56,8 +59,9 @@ var ToolbarToggler = (function(my) {
if (!isToolbarHover) {
header.hide("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "-=40"}, 300);
if($("#remoteVideos").hasClass("hidden")) {
bottomToolbar.hide("slide", {direction: "right", duration: 300});
if ($("#remoteVideos").hasClass("hidden")) {
bottomToolbar.hide(
"slide", {direction: "right", duration: 300});
}
}
else {
@ -71,7 +75,7 @@ var ToolbarToggler = (function(my) {
*
* @param isDock indicates what operation to perform
*/
my.dockToolbar = function(isDock) {
my.dockToolbar = function (isDock) {
if (isDock) {
// First make sure the toolbar is shown.
if (!$('#header').is(':visible')) {

15
util.js
View File

@ -82,5 +82,20 @@ var Util = (function (my) {
element.setAttribute("data-container", "body");
};
my.createExpBackoffTimer = function (step) {
var count = 1;
return function (reset) {
// Reset call
if (reset) {
count = 1;
return;
}
// Calculate next timeout
var timeout = Math.pow(2, count - 1);
count += 1;
return timeout * step;
};
};
return my;
}(Util || {}));

View File

@ -30,6 +30,10 @@ var VideoLayout = (function (my) {
RTC.attachMediaStream($('#localAudio'), stream);
document.getElementById('localAudio').autoplay = true;
document.getElementById('localAudio').volume = 0;
if (preMuted) {
setAudioMuted(true);
preMuted = false;
}
};
my.changeLocalVideo = function(stream, flipX) {
@ -438,7 +442,7 @@ var VideoLayout = (function (my) {
if ($('#' + videoSpanId).length > 0) {
// If there's been a focus change, make sure we add focus related
// interface!!
if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0) {
if (Moderator.isModerator() && $('#remote_popupmenu_' + resourceJid).length <= 0) {
addRemoteVideoMenu(peerJid,
document.getElementById(videoSpanId));
}
@ -476,7 +480,7 @@ var VideoLayout = (function (my) {
// If the peerJid is null then this video span couldn't be directly
// associated with a participant (this could happen in the case of prezi).
if (focus && peerJid != null)
if (Moderator.isModerator() && peerJid !== null)
addRemoteVideoMenu(peerJid, container);
remotes.appendChild(container);
@ -844,42 +848,47 @@ var VideoLayout = (function (my) {
};
/**
* Shows a visual indicator for the focus of the conference.
* Currently if we're not the owner of the conference we obtain the focus
* from the connection.jingle.sessions.
* Shows a visual indicator for the moderator of the conference.
*/
my.showFocusIndicator = function() {
if (focus !== null) {
my.showModeratorIndicator = function () {
if (Moderator.isModerator()) {
var indicatorSpan = $('#localVideoContainer .focusindicator');
if (indicatorSpan.children().length === 0)
{
createFocusIndicatorElement(indicatorSpan[0]);
createModeratorIndicatorElement(indicatorSpan[0]);
}
}
else if (Object.keys(connection.jingle.sessions).length > 0) {
// If we're only a participant the focus will be the only session we have.
var session
= connection.jingle.sessions
[Object.keys(connection.jingle.sessions)[0]];
var focusId
= 'participant_' + Strophe.getResourceFromJid(session.peerjid);
} else {
Object.keys(connection.emuc.members).forEach(function (jid) {
var member = connection.emuc.members[jid];
if (member.role === 'moderator') {
var moderatorId
= 'participant_' + Strophe.getResourceFromJid(jid);
var focusContainer = document.getElementById(focusId);
if (!focusContainer) {
console.error("No focus container!");
return;
}
var indicatorSpan = $('#' + focusId + ' .focusindicator');
var moderatorContainer
= document.getElementById(moderatorId);
if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator';
if (Strophe.getResourceFromJid(jid) === 'focus') {
// Skip server side focus
return;
}
if (!moderatorContainer) {
console.error("No moderator container for " + jid);
return;
}
var indicatorSpan
= $('#' + moderatorId + ' .focusindicator');
focusContainer.appendChild(indicatorSpan);
if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator';
createFocusIndicatorElement(indicatorSpan);
}
moderatorContainer.appendChild(indicatorSpan);
createModeratorIndicatorElement(indicatorSpan);
}
}
});
}
};
@ -1197,15 +1206,15 @@ var VideoLayout = (function (my) {
}
/**
* Creates the element indicating the focus of the conference.
* Creates the element indicating the moderator(owner) of the conference.
*
* @param parentElement the parent element where the focus indicator will
* @param parentElement the parent element where the owner indicator will
* be added
*/
function createFocusIndicatorElement(parentElement) {
var focusIndicator = document.createElement('i');
focusIndicator.className = 'fa fa-star';
parentElement.appendChild(focusIndicator);
function createModeratorIndicatorElement(parentElement) {
var moderatorIndicator = document.createElement('i');
moderatorIndicator.className = 'fa fa-star';
parentElement.appendChild(moderatorIndicator);
Util.setTooltip(parentElement,
"The owner of<br/>this conference",
@ -1308,8 +1317,8 @@ var VideoLayout = (function (my) {
if ($(this).attr('disabled') != undefined) {
event.preventDefault();
}
var isMute = !mutedAudios[jid];
connection.moderate.setMute(jid, isMute);
var isMute = mutedAudios[jid] == true;
connection.moderate.setMute(jid, !isMute);
popupmenuElement.setAttribute('style', 'display:none;');
if (isMute) {
@ -1389,19 +1398,27 @@ var VideoLayout = (function (my) {
* On audio muted event.
*/
$(document).bind('audiomuted.muc', function (event, jid, isMuted) {
/*
// FIXME: but focus can not mute in this case ? - check
if (jid === connection.emuc.myroomjid) {
// The local mute indicator is controlled locally
return;
}*/
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
videoSpanId = 'localVideoContainer';
} else {
VideoLayout.ensurePeerContainerExists(jid);
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
}
VideoLayout.ensurePeerContainerExists(jid);
mutedAudios[jid] = isMuted;
if (focus) {
mutedAudios[jid] = isMuted;
if (Moderator.isModerator()) {
VideoLayout.updateRemoteVideoMenu(jid, isMuted);
}
var videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
if (videoSpanId)
VideoLayout.showAudioIndicator(videoSpanId, isMuted);
});
@ -1665,7 +1682,7 @@ var VideoLayout = (function (my) {
VideoLayout.updateLargeVideo(RTC.getVideoSrc(videoelem[0]), 1, parentResourceJid);
}
VideoLayout.showFocusIndicator();
VideoLayout.showModeratorIndicator();
}
});