From cc761700fe5c68b1d7af7be350c2a67cc850a786 Mon Sep 17 00:00:00 2001 From: yanas Date: Wed, 23 Mar 2016 20:43:29 -0500 Subject: [PATCH] Extends the follow-me feature by adding the possibility to follow the pinned participant, the shared video and the shared document. Adds the possibility to enable disable follow-me from the settings panel. Some other small fixes throughout the UI. --- conference.js | 18 +- css/settingsmenu.css | 18 +- index.html | 6 + lang/main.json | 3 +- modules/FollowMe.js | 230 ++++++++++++++---- modules/UI/UI.js | 37 ++- modules/UI/etherpad/Etherpad.js | 16 +- modules/UI/shared_video/SharedVideo.js | 4 +- .../UI/side_pannels/settings/SettingsMenu.js | 21 ++ modules/UI/videolayout/LocalVideo.js | 2 +- modules/UI/videolayout/RemoteVideo.js | 2 +- modules/UI/videolayout/SmallVideo.js | 8 + modules/UI/videolayout/VideoLayout.js | 68 +++--- service/UI/UIEvents.js | 12 +- 14 files changed, 338 insertions(+), 107 deletions(-) diff --git a/conference.js b/conference.js index b5f6100d6..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 @@ -1030,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 6c8a831db..1d7fcb29f 100644 --- a/index.html +++ b/index.html @@ -240,6 +240,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 index 03c2a2192..dd9bd3926 100644 --- a/modules/FollowMe.js +++ b/modules/FollowMe.js @@ -1,27 +1,29 @@ -/* - * 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. +/* + * 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. */ -/* private */ const _COMMAND = "follow-me"; +const _COMMAND = "follow-me"; /** * Represents the set of {FollowMe}-related states (properties and their @@ -29,7 +31,7 @@ import UIEvents from '../service/UI/UIEvents'; * will send {_COMMAND} whenever a property of {State} changes (if the local * participant is in her right to issue such a command, of course). */ -/* private */ class State { +class State { /** * Initializes a new {State} instance. * @@ -39,13 +41,13 @@ import UIEvents from '../service/UI/UIEvents'; * the property, the old value of the property before the change, and the * new value of the property after the change. */ - /* public */ constructor (propertyChangeCallback) { - /* private*/ this._propertyChangeCallback = propertyChangeCallback; + constructor (propertyChangeCallback) { + this._propertyChangeCallback = propertyChangeCallback; } - /* public */ get filmStripVisible () { return this._filmStripVisible; } + get filmStripVisible () { return this._filmStripVisible; } - /* public */ set filmStripVisible (b) { + set filmStripVisible (b) { var oldValue = this._filmStripVisible; if (oldValue !== b) { this._filmStripVisible = b; @@ -53,6 +55,26 @@ import UIEvents from '../service/UI/UIEvents'; } } + 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}. @@ -62,7 +84,7 @@ import UIEvents from '../service/UI/UIEvents'; * @param oldValue the value of {property} before the change * @param newValue the value of {property} after the change */ - /* private */ _firePropertyChange (property, oldValue, newValue) { + _firePropertyChange (property, oldValue, newValue) { var propertyChangeCallback = this._propertyChangeCallback; if (propertyChangeCallback) propertyChangeCallback(property, oldValue, newValue); @@ -74,9 +96,9 @@ import UIEvents from '../service/UI/UIEvents'; * (partially) control the user experience/interface (e.g. film strip * visibility) of (other) non-moderator particiapnts. * - * @author Lyubomir Marinov + * @author Lyubomir Marinov */ -/* public */ class FollowMe { +class FollowMe { /** * Initializes a new {FollowMe} instance. * @@ -87,15 +109,14 @@ import UIEvents from '../service/UI/UIEvents'; * destination (model/state) to receive from the remote moderator if the * local participant is not the moderator */ - /* public */ constructor (conference, UI) { - /* private */ this._conference = conference; - /* private */ this._UI = UI; + 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). - /* private */ this._local - = new State(this._localPropertyChange.bind(this)); + 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 @@ -104,18 +125,57 @@ import UIEvents from '../service/UI/UIEvents'; conference.commands.addCommandListener( _COMMAND, this._onFollowMeCommand.bind(this)); - // Listen to (user interface) 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. - UI.addListener( - UIEvents.TOGGLED_FILM_STRIP, - this._filmStripToggled.bind(this)); - // TODO Listen to changes in the moderator role of the local - // participant. When the local participant is granted the moderator - // role, it may need to start sending "Follow Me" commands. Obviously, - // this depends on how we decide to enable the feature at runtime as - // well. + } + + /** + * 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(); } /** @@ -125,11 +185,49 @@ import UIEvents from '../service/UI/UIEvents'; * @param filmStripVisible {Boolean} {true} if the film strip was shown (as * a result of the toggle) or {false} if the film strip was hidden */ - /* private */ _filmStripToggled (filmStripVisible) { + _filmStripToggled (filmStripVisible) { this._local.filmStripVisible = filmStripVisible; } - /* private */ _localPropertyChange (property, oldValue, newValue) { + /** + * 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) @@ -141,12 +239,14 @@ import UIEvents from '../service/UI/UIEvents'; // sendCommand! commands.removeCommand(_COMMAND); var self = this; - commands.sendCommand( + commands.sendCommandOnce( _COMMAND, { attributes: { filmStripVisible: self._local.filmStripVisible, - }, + nextOnStage: self._local.nextOnStage, + sharedDocumentVisible: self._local.sharedDocumentVisible + } }); } @@ -159,7 +259,7 @@ import UIEvents from '../service/UI/UIEvents'; * notable idiosyncrasy of the Command(s) API to be mindful of here is that * the command may be issued by the local participant. */ - /* private */ _onFollowMeCommand ({ attributes }, id) { + _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. @@ -169,28 +269,60 @@ import UIEvents from '../service/UI/UIEvents'; // to act upon them. if (this._conference.isLocalId(id)) return; + // TODO Don't obey commands issued by non-moderators. - // Apply the received/remote command to the user experience/interface + // 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); + } - // filmStripVisible - var filmStripVisible = attributes.filmStripVisible; + _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. - this._UI.eventEmitter.emit( + 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 b462f6905..a914e1d24 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -35,6 +35,8 @@ UI.eventEmitter = eventEmitter; let etherpadManager; let sharedVideoManager; +let followMeHandler; + /** * Prompt user for nickname. */ @@ -252,7 +254,7 @@ UI.initConference = function () { // 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. - new FollowMe(APP.conference, UI); + followMeHandler = new FollowMe(APP.conference, UI); }; UI.mucJoined = function () { @@ -290,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); + }); } /** @@ -478,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 @@ -549,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"); @@ -686,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/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 5ac92593b..25606bb5e 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 b5452ea5f..5c091481c 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -56,14 +56,20 @@ export default { * * @see {TOGGLE_FILM_STRIP} */ - TOGGLED_FILM_STRIP: "UI.toggled_fim_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" };