From 972fc402e4924779cc3c4fa68d19f27d77b2c5a4 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Fri, 11 Mar 2016 04:49:13 -0600 Subject: [PATCH 1/4] Exposes a Command(s) API from conference.js. --- conference.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/conference.js b/conference.js index 529a6c1aa..2e12795fd 100644 --- a/conference.js +++ b/conference.js @@ -506,6 +506,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) { From c35590dbda7d9b0b7a6dac67ce5e79a088a0261a Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Fri, 11 Mar 2016 04:54:06 -0600 Subject: [PATCH 2/4] Allows UI.toggleFilmStrip() and UIEvents.TOGGLE_FILM_STRIP to act as setters in addition to toggles. --- modules/UI/UI.js | 3 ++- modules/UI/videolayout/FilmStrip.js | 15 ++++++++++++++- service/UI/UIEvents.js | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index e68113ff2..c6e0f9cc4 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -583,7 +583,8 @@ UI.toggleSmileys = function () { * Toggles film strip. */ UI.toggleFilmStrip = function () { - FilmStrip.toggleFilmStrip(); + var self = FilmStrip; + self.toggleFilmStrip.apply(self, arguments); }; /** diff --git a/modules/UI/videolayout/FilmStrip.js b/modules/UI/videolayout/FilmStrip.js index 2e0a8ce50..ccb1783aa 100644 --- a/modules/UI/videolayout/FilmStrip.js +++ b/modules/UI/videolayout/FilmStrip.js @@ -9,7 +9,20 @@ const FilmStrip = { this.filmStrip = $('#remoteVideos'); }, - 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"); }, diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index b915b3793..5d1586193 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -34,6 +34,17 @@ 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", TOGGLE_SCREENSHARING: "UI.toggle_screensharing", CONTACT_CLICKED: "UI.contact_clicked", From 605a892f789a4fed9b08c9515474d1c2a83d6c6e Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Fri, 11 Mar 2016 04:55:29 -0600 Subject: [PATCH 3/4] Implements an initial (demo) version of "Follow Me" for film strip visibility. --- modules/FollowMe.js | 196 ++++++++++++++++++++++++++++ modules/UI/UI.js | 10 +- modules/UI/videolayout/FilmStrip.js | 17 ++- service/UI/UIEvents.js | 8 ++ 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 modules/FollowMe.js diff --git a/modules/FollowMe.js b/modules/FollowMe.js new file mode 100644 index 000000000..03c2a2192 --- /dev/null +++ b/modules/FollowMe.js @@ -0,0 +1,196 @@ +/* + * 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'; + +/** + * 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"; + +/** + * 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). + */ +/* private */ 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. + */ + /* public */ constructor (propertyChangeCallback) { + /* private*/ this._propertyChangeCallback = propertyChangeCallback; + } + + /* public */ get filmStripVisible () { return this._filmStripVisible; } + + /* public */ set filmStripVisible (b) { + var oldValue = this._filmStripVisible; + if (oldValue !== b) { + this._filmStripVisible = b; + this._firePropertyChange('filmStripVisible', 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 + */ + /* private */ _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 + */ +/* public */ 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 + */ + /* public */ constructor (conference, UI) { + /* private */ this._conference = conference; + /* private */ 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)); + + // 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)); + // 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. + } + + /** + * 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 + */ + /* private */ _filmStripToggled (filmStripVisible) { + this._local.filmStripVisible = filmStripVisible; + } + + /* 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.sendCommand( + _COMMAND, + { + attributes: { + filmStripVisible: self._local.filmStripVisible, + }, + }); + } + + /** + * 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. + */ + /* private */ _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. + + // Apply the received/remote command to the user experience/interface + // of the local participant. + + // filmStripVisible + var filmStripVisible = attributes.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( + UIEvents.TOGGLE_FILM_STRIP, + filmStripVisible); + } + } +} + +export default FollowMe; diff --git a/modules/UI/UI.js b/modules/UI/UI.js index c6e0f9cc4..338bcbc34 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -27,6 +27,8 @@ var messageHandler = UI.messageHandler; var JitsiPopover = require("./util/JitsiPopover"); var Feedback = require("./Feedback"); +import FollowMe from "../FollowMe"; + var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; @@ -246,6 +248,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. + new FollowMe(APP.conference, UI); }; UI.mucJoined = function () { @@ -326,7 +334,7 @@ UI.start = function () { registerListeners(); BottomToolbar.init(); - FilmStrip.init(); + FilmStrip.init(eventEmitter); VideoLayout.init(eventEmitter); if (!interfaceConfig.filmStripOnly) { diff --git a/modules/UI/videolayout/FilmStrip.js b/modules/UI/videolayout/FilmStrip.js index ccb1783aa..3d680fa98 100644 --- a/modules/UI/videolayout/FilmStrip.js +++ b/modules/UI/videolayout/FilmStrip.js @@ -1,12 +1,19 @@ /* global $, APP, interfaceConfig, config*/ +import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from "../util/UIUtil"; const thumbAspectRatio = 16.0 / 9.0; 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; }, /** @@ -24,6 +31,14 @@ const FilmStrip = { } 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/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5d1586193..cb42d8711 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -46,6 +46,14 @@ export default { * @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_fim_strip", TOGGLE_SCREENSHARING: "UI.toggle_screensharing", CONTACT_CLICKED: "UI.contact_clicked", HANGUP: "UI.hangup", From cc761700fe5c68b1d7af7be350c2a67cc850a786 Mon Sep 17 00:00:00 2001 From: yanas Date: Wed, 23 Mar 2016 20:43:29 -0500 Subject: [PATCH 4/4] 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 @@ +
+ +