Implements an initial (demo) version of "Follow Me" for film strip visibility.

This commit is contained in:
Lyubomir Marinov 2016-03-11 04:55:29 -06:00
parent c35590dbda
commit 605a892f78
4 changed files with 229 additions and 2 deletions

196
modules/FollowMe.js Normal file
View File

@ -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;

View File

@ -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) {

View File

@ -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 () {

View File

@ -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",