diff --git a/conference.js b/conference.js
index ad08b8a53..a59cfd429 100644
--- a/conference.js
+++ b/conference.js
@@ -31,6 +31,8 @@ const Commands = {
SHARED_VIDEO: "shared-video"
};
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
+
/**
* Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use
@@ -509,6 +511,51 @@ export default {
this._setupListeners();
},
+
+ /**
+ * Exposes a Command(s) API on this instance. It is necessitated by (1) the
+ * desire to keep room private to this instance and (2) the need of other
+ * modules to send and receive commands to and from participants.
+ * Eventually, this instance remains in control with respect to the
+ * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's
+ * JitsiConference) is to be used in the implementation of the Command(s)
+ * API of this instance.
+ */
+ commands: {
+ /**
+ * Receives notifications from other participants about commands aka
+ * custom events (sent by sendCommand or sendCommandOnce methods).
+ * @param command {String} the name of the command
+ * @param handler {Function} handler for the command
+ */
+ addCommandListener () {
+ room.addCommandListener.apply(room, arguments);
+ },
+ /**
+ * Removes command.
+ * @param name {String} the name of the command.
+ */
+ removeCommand () {
+ room.removeCommand.apply(room, arguments);
+ },
+ /**
+ * Sends command.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommand () {
+ room.sendCommand.apply(room, arguments);
+ },
+ /**
+ * Sends command one time.
+ * @param name {String} the name of the command.
+ * @param values {Object} with keys and values that will be sent.
+ */
+ sendCommandOnce () {
+ room.sendCommandOnce.apply(room, arguments);
+ },
+ },
+
_getConferenceOptions() {
let options = config;
if(config.enableRecording) {
@@ -985,8 +1032,20 @@ export default {
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT, (id) => {
room.selectParticipant(id);
});
- APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (id) => {
- room.pinParticipant(id);
+
+ APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
+ var smallVideoId = smallVideo.getId();
+
+ if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
+ && !APP.conference.isLocalId(smallVideoId))
+ if (isPinned)
+ room.pinParticipant(smallVideoId);
+ // When the library starts supporting multiple pins we would
+ // pass the isPinned parameter together with the identifier,
+ // but currently we send null to indicate that we unpin the
+ // last pinned.
+ else
+ room.pinParticipant(null);
});
APP.UI.addListener(
diff --git a/css/settingsmenu.css b/css/settingsmenu.css
index c990dfb4a..1cd63203e 100644
--- a/css/settingsmenu.css
+++ b/css/settingsmenu.css
@@ -43,29 +43,25 @@
cursor: pointer;
}
-
-#startMutedOptions {
+#startMutedOptions,
+#followMeOptions {
padding-left: 10%;
text-indent: -10%;
-
margin-top: 10px;
-
display: none; /* hide by default */
-
/* clearfix */
overflow: auto;
zoom: 1;
}
-#startAudioMuted {
+#startAudioMuted,
+#startVideoMuted,
+#followMeCheckBox {
width: 13px !important;
}
-#startVideoMuted {
- width: 13px !important;
-}
-
-.startMutedLabel {
+.startMutedLabel,
+.followMeLabel {
width: 94%;
float: left;
cursor: pointer;
diff --git a/index.html b/index.html
index 8f530f534..b77017611 100644
--- a/index.html
+++ b/index.html
@@ -241,6 +241,12 @@
+
+
+
diff --git a/lang/main.json b/lang/main.json
index 5125714df..f03c1bbc3 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -86,7 +86,8 @@
"startAudioMuted": "start without audio",
"startVideoMuted": "start without video",
"selectCamera": "select camera",
- "selectMic": "select microphone"
+ "selectMic": "select microphone",
+ "followMe": "Enable follow me"
},
"videothumbnail":
{
diff --git a/modules/FollowMe.js b/modules/FollowMe.js
new file mode 100644
index 000000000..dd9bd3926
--- /dev/null
+++ b/modules/FollowMe.js
@@ -0,0 +1,328 @@
+/*
+ * Copyright @ 2015 Atlassian Pty Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UIEvents from '../service/UI/UIEvents';
+import VideoLayout from './UI/videolayout/VideoLayout';
+import FilmStrip from './UI/videolayout/FilmStrip';
+
+/**
+ * The (name of the) command which transports the state (represented by
+ * {State} for the local state at the time of this writing) of a {FollowMe}
+ * (instance) between participants.
+ */
+const _COMMAND = "follow-me";
+
+/**
+ * Represents the set of {FollowMe}-related states (properties and their
+ * respective values) which are to be followed by a participant. {FollowMe}
+ * will send {_COMMAND} whenever a property of {State} changes (if the local
+ * participant is in her right to issue such a command, of course).
+ */
+class State {
+ /**
+ * Initializes a new {State} instance.
+ *
+ * @param propertyChangeCallback {Function} which is to be called when a
+ * property of the new instance has its value changed from an old value
+ * into a (different) new value. The function is supplied with the name of
+ * the property, the old value of the property before the change, and the
+ * new value of the property after the change.
+ */
+ constructor (propertyChangeCallback) {
+ this._propertyChangeCallback = propertyChangeCallback;
+ }
+
+ get filmStripVisible () { return this._filmStripVisible; }
+
+ set filmStripVisible (b) {
+ var oldValue = this._filmStripVisible;
+ if (oldValue !== b) {
+ this._filmStripVisible = b;
+ this._firePropertyChange('filmStripVisible', oldValue, b);
+ }
+ }
+
+ get nextOnStage() { return this._nextOnStage; }
+
+ set nextOnStage(id) {
+ var oldValue = this._nextOnStage;
+ if (oldValue !== id) {
+ this._nextOnStage = id;
+ this._firePropertyChange('nextOnStage', oldValue, id);
+ }
+ }
+
+ get sharedDocumentVisible () { return this._sharedDocumentVisible; }
+
+ set sharedDocumentVisible (b) {
+ var oldValue = this._sharedDocumentVisible;
+ if (oldValue !== b) {
+ this._sharedDocumentVisible = b;
+ this._firePropertyChange('sharedDocumentVisible', oldValue, b);
+ }
+ }
+
+ /**
+ * Invokes {_propertyChangeCallback} to notify it that {property} had its
+ * value changed from {oldValue} to {newValue}.
+ *
+ * @param property the name of the property which had its value changed
+ * from {oldValue} to {newValue}
+ * @param oldValue the value of {property} before the change
+ * @param newValue the value of {property} after the change
+ */
+ _firePropertyChange (property, oldValue, newValue) {
+ var propertyChangeCallback = this._propertyChangeCallback;
+ if (propertyChangeCallback)
+ propertyChangeCallback(property, oldValue, newValue);
+ }
+}
+
+/**
+ * Represents the "Follow Me" feature which enables a moderator to
+ * (partially) control the user experience/interface (e.g. film strip
+ * visibility) of (other) non-moderator particiapnts.
+ *
+ * @author Lyubomir Marinov
+ */
+class FollowMe {
+ /**
+ * Initializes a new {FollowMe} instance.
+ *
+ * @param conference the {conference} which is to transport
+ * {FollowMe}-related information between participants
+ * @param UI the {UI} which is the source (model/state) to be sent to
+ * remote participants if the local participant is the moderator or the
+ * destination (model/state) to receive from the remote moderator if the
+ * local participant is not the moderator
+ */
+ constructor (conference, UI) {
+ this._conference = conference;
+ this._UI = UI;
+
+ // The states of the local participant which are to be followed (by the
+ // remote participants when the local participant is in her right to
+ // issue such commands).
+ this._local = new State(this._localPropertyChange.bind(this));
+
+ // Listen to "Follow Me" commands. I'm not sure whether a moderator can
+ // (in lib-jitsi-meet and/or Meet) become a non-moderator. If that's
+ // possible, then it may be easiest to always listen to commands. The
+ // listener will validate received commands before acting on them.
+ conference.commands.addCommandListener(
+ _COMMAND,
+ this._onFollowMeCommand.bind(this));
+ }
+
+ /**
+ * Adds listeners for the UI states of the local participant which are
+ * to be followed (by the remote participants). A non-moderator (very
+ * likely) can become a moderator so it may be easiest to always track
+ * the states of interest.
+ * @private
+ */
+ _addFollowMeListeners () {
+ this.filmStripEventHandler = this._filmStripToggled.bind(this);
+ this._UI.addListener(UIEvents.TOGGLED_FILM_STRIP,
+ this.filmStripEventHandler);
+
+ var self = this;
+ this.pinnedEndpointEventHandler = function (smallVideo, isPinned) {
+ self._nextOnStage(smallVideo, isPinned);
+ };
+ this._UI.addListener(UIEvents.PINNED_ENDPOINT,
+ this.pinnedEndpointEventHandler);
+
+ this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
+ this._UI.addListener( UIEvents.TOGGLED_SHARED_DOCUMENT,
+ this.sharedDocEventHandler);
+ }
+
+ /**
+ * Removes all follow me listeners.
+ * @private
+ */
+ _removeFollowMeListeners () {
+ this._UI.removeListener(UIEvents.TOGGLED_FILM_STRIP,
+ this.filmStripEventHandler);
+ this._UI.removeListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
+ this.sharedDocEventHandler);
+ this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
+ this.pinnedEndpointEventHandler);
+ }
+
+ /**
+ * Enables or disabled the follow me functionality
+ *
+ * @param enable {true} to enable the follow me functionality, {false} -
+ * to disable it
+ */
+ enableFollowMe (enable) {
+ this.isEnabled = enable;
+ if (this.isEnabled)
+ this._addFollowMeListeners();
+ else
+ this._removeFollowMeListeners();
+ }
+
+ /**
+ * Notifies this instance that the (visibility of the) film strip was
+ * toggled (in the user interface of the local participant).
+ *
+ * @param filmStripVisible {Boolean} {true} if the film strip was shown (as
+ * a result of the toggle) or {false} if the film strip was hidden
+ */
+ _filmStripToggled (filmStripVisible) {
+ this._local.filmStripVisible = filmStripVisible;
+ }
+
+ /**
+ * Notifies this instance that the (visibility of the) shared document was
+ * toggled (in the user interface of the local participant).
+ *
+ * @param sharedDocumentVisible {Boolean} {true} if the shared document was
+ * shown (as a result of the toggle) or {false} if it was hidden
+ */
+ _sharedDocumentToggled (sharedDocumentVisible) {
+ this._local.sharedDocumentVisible = sharedDocumentVisible;
+ }
+
+ /**
+ * Changes the nextOnPage property value.
+ *
+ * @param smallVideo the {SmallVideo} that was pinned or unpinned
+ * @param isPinned indicates if the given {SmallVideo} was pinned or
+ * unpinned
+ * @private
+ */
+ _nextOnStage (smallVideo, isPinned) {
+ if (!this._conference.isModerator)
+ return;
+
+ var nextOnStage = null;
+ if(isPinned)
+ nextOnStage = smallVideo.getId();
+
+ this._local.nextOnStage = nextOnStage;
+ }
+
+ /**
+ * Sends the follow-me command, when a local property change occurs.
+ *
+ * @param property the property name
+ * @param oldValue the old value
+ * @param newValue the new value
+ * @private
+ */
+ _localPropertyChange (property, oldValue, newValue) {
+ // Only a moderator is allowed to send commands.
+ var conference = this._conference;
+ if (!conference.isModerator)
+ return;
+
+ var commands = conference.commands;
+ // XXX The "Follow Me" command represents a snapshot of all states
+ // which are to be followed so don't forget to removeCommand before
+ // sendCommand!
+ commands.removeCommand(_COMMAND);
+ var self = this;
+ commands.sendCommandOnce(
+ _COMMAND,
+ {
+ attributes: {
+ filmStripVisible: self._local.filmStripVisible,
+ nextOnStage: self._local.nextOnStage,
+ sharedDocumentVisible: self._local.sharedDocumentVisible
+ }
+ });
+ }
+
+ /**
+ * Notifies this instance about a &qout;Follow Me&qout; command (delivered
+ * by the Command(s) API of {this._conference}).
+ *
+ * @param attributes the attributes {Object} carried by the command
+ * @param id the identifier of the participant who issued the command. A
+ * notable idiosyncrasy of the Command(s) API to be mindful of here is that
+ * the command may be issued by the local participant.
+ */
+ _onFollowMeCommand ({ attributes }, id) {
+ // We require to know who issued the command because (1) only a
+ // moderator is allowed to send commands and (2) a command MUST be
+ // issued by a defined commander.
+ if (typeof id === 'undefined')
+ return;
+ // The Command(s) API will send us our own commands and we don't want
+ // to act upon them.
+ if (this._conference.isLocalId(id))
+ return;
+
+ // TODO Don't obey commands issued by non-moderators.
+
+ // Applies the received/remote command to the user experience/interface
+ // of the local participant.
+ this._onFilmStripVisible(attributes.filmStripVisible);
+ this._onNextOnStage(attributes.nextOnStage);
+ this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
+ }
+
+ _onFilmStripVisible(filmStripVisible) {
+ if (typeof filmStripVisible !== 'undefined') {
+ // XXX The Command(s) API doesn't preserve the types (of
+ // attributes, at least) at the time of this writing so take into
+ // account that what originated as a Boolean may be a String on
+ // receipt.
+ filmStripVisible = (filmStripVisible == 'true');
+
+ // FIXME The UI (module) very likely doesn't (want to) expose its
+ // eventEmitter as a public field. I'm not sure at the time of this
+ // writing whether calling UI.toggleFilmStrip() is acceptable (from
+ // a design standpoint) either.
+ if (filmStripVisible !== FilmStrip.isFilmStripVisible())
+ this._UI.eventEmitter.emit(
+ UIEvents.TOGGLE_FILM_STRIP,
+ filmStripVisible);
+ }
+ }
+
+ _onNextOnStage(id) {
+
+ var clickId = null;
+ if(typeof id !== 'undefined' && !VideoLayout.isPinned(id))
+ clickId = id;
+ else if (typeof id == 'undefined')
+ clickId = VideoLayout.getPinnedId();
+
+ if (clickId !== null)
+ VideoLayout.handleVideoThumbClicked(clickId);
+ }
+
+ _onSharedDocumentVisible(sharedDocumentVisible) {
+ if (typeof sharedDocumentVisible !== 'undefined') {
+ // XXX The Command(s) API doesn't preserve the types (of
+ // attributes, at least) at the time of this writing so take into
+ // account that what originated as a Boolean may be a String on
+ // receipt.
+ sharedDocumentVisible = (sharedDocumentVisible == 'true');
+
+ if (sharedDocumentVisible
+ !== this._UI.getSharedDocumentManager().isVisible())
+ this._UI.getSharedDocumentManager().toggleEtherpad();
+ }
+ }
+}
+
+export default FollowMe;
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 347776d40..a914e1d24 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -27,12 +27,16 @@ var messageHandler = UI.messageHandler;
var JitsiPopover = require("./util/JitsiPopover");
var Feedback = require("./Feedback");
+import FollowMe from "../FollowMe";
+
var eventEmitter = new EventEmitter();
UI.eventEmitter = eventEmitter;
let etherpadManager;
let sharedVideoManager;
+let followMeHandler;
+
/**
* Prompt user for nickname.
*/
@@ -245,6 +249,12 @@ UI.initConference = function () {
if(!interfaceConfig.filmStripOnly) {
Feedback.init();
}
+
+ // FollowMe attempts to copy certain aspects of the moderator's UI into the
+ // other participants' UI. Consequently, it needs (1) read and write access
+ // to the UI (depending on the moderator role of the local participant) and
+ // (2) APP.conference as means of communication between the participants.
+ followMeHandler = new FollowMe(APP.conference, UI);
};
UI.mucJoined = function () {
@@ -282,6 +292,11 @@ function registerListeners() {
UI.toggleFilmStrip();
VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false);
});
+
+ UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
+ if (followMeHandler)
+ followMeHandler.enableFollowMe(isEnabled);
+ });
}
/**
@@ -331,7 +346,7 @@ UI.start = function () {
registerListeners();
BottomToolbar.init();
- FilmStrip.init();
+ FilmStrip.init(eventEmitter);
VideoLayout.init(eventEmitter);
if (!interfaceConfig.filmStripOnly) {
@@ -470,10 +485,19 @@ UI.initEtherpad = function (name) {
return;
}
console.log('Etherpad is enabled');
- etherpadManager = new EtherpadManager(config.etherpad_base, name);
+ etherpadManager
+ = new EtherpadManager(config.etherpad_base, name, eventEmitter);
Toolbar.showEtherpadButton();
};
+/**
+ * Returns the shared document manager object.
+ * @return {EtherpadManager} the shared document manager object
+ */
+UI.getSharedDocumentManager = function () {
+ return etherpadManager;
+};
+
/**
* Show user on UI.
* @param {string} id user id
@@ -541,6 +565,7 @@ UI.updateLocalRole = function (isModerator) {
Toolbar.showRecordingButton(isModerator);
Toolbar.showSharedVideoButton(isModerator);
SettingsMenu.showStartMutedOptions(isModerator);
+ SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
@@ -589,7 +614,8 @@ UI.toggleSmileys = function () {
* Toggles film strip.
*/
UI.toggleFilmStrip = function () {
- FilmStrip.toggleFilmStrip();
+ var self = FilmStrip;
+ self.toggleFilmStrip.apply(self, arguments);
};
/**
@@ -677,10 +703,26 @@ UI.setVideoMuted = function (id, muted) {
}
};
+/**
+ * Adds a listener that would be notified on the given type of event.
+ *
+ * @param type the type of the event we're listening for
+ * @param listener a function that would be called when notified
+ */
UI.addListener = function (type, listener) {
eventEmitter.on(type, listener);
};
+/**
+ * Removes the given listener for the given type of event.
+ *
+ * @param type the type of the event we're listening for
+ * @param listener the listener we want to remove
+ */
+UI.removeListener = function (type, listener) {
+ eventEmitter.removeListener(type, listener);
+};
+
UI.clickOnVideo = function (videoNumber) {
var remoteVideos = $(".videocontainer:not(#mixedstream)");
if (remoteVideos.length > videoNumber) {
diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js
index 7c65016a5..58315c9a9 100644
--- a/modules/UI/etherpad/Etherpad.js
+++ b/modules/UI/etherpad/Etherpad.js
@@ -3,6 +3,7 @@
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
+import UIEvents from "../../../service/UI/UIEvents";
import SidePanelToggler from "../side_pannels/SidePanelToggler";
import FilmStrip from '../videolayout/FilmStrip';
@@ -58,6 +59,7 @@ const ETHERPAD_CONTAINER_TYPE = "etherpad";
* Container for Etherpad iframe.
*/
class Etherpad extends LargeContainer {
+
constructor (domain, name) {
super();
@@ -149,13 +151,14 @@ class Etherpad extends LargeContainer {
* Manager of the Etherpad frame.
*/
export default class EtherpadManager {
- constructor (domain, name) {
+ constructor (domain, name, eventEmitter) {
if (!domain || !name) {
throw new Error("missing domain or name");
}
this.domain = domain;
this.name = name;
+ this.eventEmitter = eventEmitter;
this.etherpad = null;
}
@@ -163,6 +166,10 @@ export default class EtherpadManager {
return !!this.etherpad;
}
+ isVisible() {
+ return VideoLayout.isLargeContainerTypeVisible(ETHERPAD_CONTAINER_TYPE);
+ }
+
/**
* Create new Etherpad frame.
*/
@@ -183,11 +190,12 @@ export default class EtherpadManager {
this.openEtherpad();
}
- let isVisible = VideoLayout.isLargeContainerTypeVisible(
- ETHERPAD_CONTAINER_TYPE
- );
+ let isVisible = this.isVisible();
VideoLayout.showLargeVideoContainer(
ETHERPAD_CONTAINER_TYPE, !isVisible);
+
+ this.eventEmitter
+ .emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
}
}
diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js
index e65f7d12a..ce3663175 100644
--- a/modules/UI/shared_video/SharedVideo.js
+++ b/modules/UI/shared_video/SharedVideo.js
@@ -120,7 +120,7 @@ export default class SharedVideoManager {
VideoLayout.addLargeVideoContainer(
SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
- VideoLayout.handleVideoThumbClicked(true, self.url);
+ VideoLayout.handleVideoThumbClicked(self.url);
self.isSharedVideoShown = true;
@@ -372,7 +372,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
* The thumb click handler.
*/
SharedVideoThumb.prototype.videoClick = function () {
- VideoLayout.handleVideoThumbClicked(true, this.url);
+ VideoLayout.handleVideoThumbClicked(this.url);
};
/**
diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js
index b2baecd0a..266f10e78 100644
--- a/modules/UI/side_pannels/settings/SettingsMenu.js
+++ b/modules/UI/side_pannels/settings/SettingsMenu.js
@@ -89,6 +89,14 @@ export default {
);
});
+ // FOLLOW ME
+ $("#followMeOptions").change(function () {
+ let isFollowMeEnabled = $("#followMeCheckBox").is(":checked");
+ emitter.emit(
+ UIEvents.FOLLOW_ME_ENABLED,
+ isFollowMeEnabled
+ );
+ });
// LANGUAGES BOX
let languagesBox = $("#languages_selectbox");
@@ -135,6 +143,19 @@ export default {
$("#startVideoMuted").attr("checked", startVideoMuted);
},
+ /**
+ * Shows/hides the follow me options in the settings dialog.
+ *
+ * @param {boolean} show {true} to show those options, {false} to hide them
+ */
+ showFollowMeOptions (show) {
+ if (show) {
+ $("#followMeOptions").css("display", "block");
+ } else {
+ $("#followMeOptions").css("display", "none");
+ }
+ },
+
/**
* Check if settings menu is visible or not.
* @returns {boolean}
diff --git a/modules/UI/videolayout/FilmStrip.js b/modules/UI/videolayout/FilmStrip.js
index 19539f752..7bdb9245b 100644
--- a/modules/UI/videolayout/FilmStrip.js
+++ b/modules/UI/videolayout/FilmStrip.js
@@ -1,16 +1,44 @@
/* global $, APP, interfaceConfig, config*/
+import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
const thumbAspectRatio = 1 / 1;
const FilmStrip = {
- init () {
+ /**
+ *
+ * @param eventEmitter the {EventEmitter} through which {FilmStrip} is to
+ * emit/fire {UIEvents} (such as {UIEvents.TOGGLED_FILM_STRIP}).
+ */
+ init (eventEmitter) {
this.filmStrip = $('#remoteVideos');
+ this.eventEmitter = eventEmitter;
},
- toggleFilmStrip () {
+ /**
+ * Toggles the visibility of the film strip.
+ *
+ * @param visible optional {Boolean} which specifies the desired visibility
+ * of the film strip. If not specified, the visibility will be flipped
+ * (i.e. toggled); otherwise, the visibility will be set to the specified
+ * value.
+ */
+ toggleFilmStrip (visible) {
+ if (typeof visible === 'boolean'
+ && this.isFilmStripVisible() == visible) {
+ return;
+ }
+
this.filmStrip.toggleClass("hidden");
+
+ // Emit/fire UIEvents.TOGGLED_FILM_STRIP.
+ var eventEmitter = this.eventEmitter;
+ if (eventEmitter) {
+ eventEmitter.emit(
+ UIEvents.TOGGLED_FILM_STRIP,
+ this.isFilmStripVisible());
+ }
},
isFilmStripVisible () {
diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js
index 380e9d7cc..59ad9123c 100644
--- a/modules/UI/videolayout/LocalVideo.js
+++ b/modules/UI/videolayout/LocalVideo.js
@@ -158,7 +158,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
if (event.stopPropagation) {
event.stopPropagation();
}
- this.VideoLayout.handleVideoThumbClicked(true, this.id);
+ this.VideoLayout.handleVideoThumbClicked(this.id);
};
let localVideoContainerSelector = $('#localVideoContainer');
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 60c9fbcf9..37e752946 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -220,7 +220,7 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
// ignore click if it was done in popup menu
if ($(source).parents('.popupmenu').length === 0) {
- this.VideoLayout.handleVideoThumbClicked(false, this.id);
+ this.VideoLayout.handleVideoThumbClicked(this.id);
}
// On IE we need to populate this handler on video