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 diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 055bc460d..6a878e4d1 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -20,6 +20,14 @@ function setVisibility(selector, show) { } } +/** + * Returns the identifier of this small video. + * + * @returns the identifier of this small video + */ +SmallVideo.prototype.getId = function () { + return this.id; +}; /* Indicates if this small video is currently visible. * diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 63736b130..f73ae3e1b 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -31,7 +31,7 @@ var eventEmitter = null; * Currently focused video jid * @type {String} */ -var focusedVideoResourceJid = null; +var pinnedId = null; /** * On contact list item clicked. @@ -49,7 +49,7 @@ function onContactClicked (id) { if (remoteVideo.hasVideoStarted()) { // We have a video src, great! Let's update the large video // now. - VideoLayout.handleVideoThumbClicked(false, id); + VideoLayout.handleVideoThumbClicked(id); } else { // If we don't have a video src for jid, there's absolutely @@ -63,7 +63,7 @@ function onContactClicked (id) { // picked up later by the lastN changed event handler. lastNPickupId = id; - eventEmitter.emit(UIEvents.PINNED_ENDPOINT, id); + eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true); } } } @@ -209,7 +209,7 @@ var VideoLayout = { // We'll show user's avatar if he is the dominant speaker or if // his video thumbnail is pinned - if (remoteVideos[id] && (id === focusedVideoResourceJid + if (remoteVideos[id] && (id === pinnedId || id === currentDominantSpeaker)) { newId = id; } else { @@ -289,20 +289,33 @@ var VideoLayout = { return smallVideo ? smallVideo.getVideoType() : null; }, - handleVideoThumbClicked (noPinnedEndpointChangedEvent, - resourceJid) { - if(focusedVideoResourceJid) { + isPinned (id) { + return (pinnedId) ? (id === pinnedId) : false; + }, + + getPinnedId () { + return pinnedId; + }, + + /** + * Handles the click on a video thumbnail. + * + * @param id the identifier of the video thumbnail + */ + handleVideoThumbClicked (id) { + if(pinnedId) { var oldSmallVideo - = VideoLayout.getSmallVideo(focusedVideoResourceJid); + = VideoLayout.getSmallVideo(pinnedId); if (oldSmallVideo && !interfaceConfig.filmStripOnly) oldSmallVideo.focus(false); } - var smallVideo = VideoLayout.getSmallVideo(resourceJid); - // Unlock current focused. - if (focusedVideoResourceJid === resourceJid) + var smallVideo = VideoLayout.getSmallVideo(id); + + // Unpin if currently pinned. + if (pinnedId === id) { - focusedVideoResourceJid = null; + pinnedId = null; // Enable the currently set dominant speaker. if (currentDominantSpeaker) { if(smallVideo && smallVideo.hasVideo()) { @@ -310,26 +323,23 @@ var VideoLayout = { } } - if (!noPinnedEndpointChangedEvent) { - eventEmitter.emit(UIEvents.PINNED_ENDPOINT); - } + eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, false); + return; } // Lock new video - focusedVideoResourceJid = resourceJid; + pinnedId = id; // Update focused/pinned interface. - if (resourceJid) { + if (id) { if (smallVideo && !interfaceConfig.filmStripOnly) smallVideo.focus(true); - if (!noPinnedEndpointChangedEvent) { - eventEmitter.emit(UIEvents.PINNED_ENDPOINT, resourceJid); - } + eventEmitter.emit(UIEvents.PINNED_ENDPOINT, smallVideo, true); } - this.updateLargeVideo(resourceJid); + this.updateLargeVideo(id); }, /** @@ -372,10 +382,10 @@ var VideoLayout = { // Update the large video to the last added video only if there's no // current dominant, focused speaker or update it to // the current dominant speaker. - if ((!focusedVideoResourceJid && + if ((!pinnedId && !currentDominantSpeaker && this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) || - focusedVideoResourceJid === resourceJid || + pinnedId === resourceJid || (resourceJid && currentDominantSpeaker === resourceJid)) { this.updateLargeVideo(resourceJid, true); @@ -531,7 +541,7 @@ var VideoLayout = { // since we don't want to switch to local video. // Update the large video if the video source is already available, // otherwise wait for the "videoactive.jingle" event. - if (!focusedVideoResourceJid + if (!pinnedId && remoteVideo.hasVideoStarted() && !this.getCurrentlyOnLargeContainer().stayOnStage()) { this.updateLargeVideo(id); @@ -650,11 +660,7 @@ var VideoLayout = { // Clean up the lastN pickup id. lastNPickupId = null; - // Don't fire the events again, they've already - // been fired in the contact list click handler. - VideoLayout.handleVideoThumbClicked( - false, - resourceJid); + VideoLayout.handleVideoThumbClicked(resourceJid); updateLargeVideo = false; } @@ -741,9 +747,9 @@ var VideoLayout = { removeParticipantContainer (id) { // Unlock large video - if (focusedVideoResourceJid === id) { + if (pinnedId === id) { console.info("Focused video owner has left the conference"); - focusedVideoResourceJid = null; + pinnedId = null; } if (currentDominantSpeaker === id) { diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index bcb18403a..5c091481c 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -37,14 +37,39 @@ export default { TOGGLE_CHAT: "UI.toggle_chat", TOGGLE_SETTINGS: "UI.toggle_settings", TOGGLE_CONTACT_LIST: "UI.toggle_contact_list", + /** + * Notifies that a command to toggle the film strip has been issued. The + * event may optionally specify a {Boolean} (primitive) value to assign to + * the visibility of the film strip (i.e. the event may act as a setter). + * The very toggling of the film strip may or may not occurred at the time + * of the receipt of the event depending on the position of the receiving + * event listener in relation to the event listener which carries out the + * command to toggle the film strip. + * + * @see {TOGGLED_FILM_STRIP} + */ TOGGLE_FILM_STRIP: "UI.toggle_film_strip", + /** + * Notifies that the film strip was (actually) toggled. The event supplies + * a {Boolean} (primitive) value indicating the visibility of the film + * strip after the toggling (at the time of the event emission). + * + * @see {TOGGLE_FILM_STRIP} + */ + TOGGLED_FILM_STRIP: "UI.toggled_film_strip", TOGGLE_SCREENSHARING: "UI.toggle_screensharing", + TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document", CONTACT_CLICKED: "UI.contact_clicked", HANGUP: "UI.hangup", LOGOUT: "UI.logout", RECORDING_TOGGLE: "UI.recording_toggle", SIP_DIAL: "UI.sip_dial", - SUBEJCT_CHANGED: "UI.subject_changed", + SUBJECT_CHANGED: "UI.subject_changed", VIDEO_DEVICE_CHANGED: "UI.video_device_changed", - AUDIO_DEVICE_CHANGED: "UI.audio_device_changed" + AUDIO_DEVICE_CHANGED: "UI.audio_device_changed", + /** + * Notifies interested listeners that the follow-me feature is enabled or + * disabled. + */ + FOLLOW_ME_ENABLED: "UI.follow_me_enabled" };