From 972fc402e4924779cc3c4fa68d19f27d77b2c5a4 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Fri, 11 Mar 2016 04:49:13 -0600 Subject: [PATCH 01/51] 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 02/51] 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 03/51] 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 4ff6d276ce86b54be1d0566091474702c710166f Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 11 Mar 2016 12:00:10 -0600 Subject: [PATCH 04/51] Removes prezi. --- README.md | 2 +- conference.js | 34 +- css/font.css | 3 - index.html | 2 - interface_config.js | 2 +- lang/main.json | 7 - modules/UI/UI.js | 27 -- modules/UI/prezi/Prezi.js | 448 -------------------------- modules/UI/prezi/PreziPlayer.js | 290 ----------------- modules/UI/toolbars/Toolbar.js | 14 - modules/UI/util/UIUtil.js | 4 +- modules/UI/videolayout/VideoLayout.js | 10 +- service/UI/UIEvents.js | 4 - 13 files changed, 6 insertions(+), 841 deletions(-) delete mode 100644 modules/UI/prezi/Prezi.js delete mode 100644 modules/UI/prezi/PreziPlayer.js diff --git a/README.md b/README.md index 7081f2772..07a4f8ddb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [J You can also try it out yourself at https://meet.jit.si . -Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad and remote presentations with Prezi. +Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad. ## Installation diff --git a/conference.js b/conference.js index 529a6c1aa..4790c3db6 100644 --- a/conference.js +++ b/conference.js @@ -27,9 +27,7 @@ let room, connection, localAudio, localVideo, roomLocker; const Commands = { CONNECTION_QUALITY: "stats", EMAIL: "email", - ETHERPAD: "etherpad", - PREZI: "prezi", - STOP_PREZI: "stop-prezi" + ETHERPAD: "etherpad" }; /** @@ -687,7 +685,6 @@ export default { console.log('USER %s LEFT', id, user); APP.API.notifyUserLeft(id); APP.UI.removeUser(id, user.getDisplayName()); - APP.UI.stopPrezi(id); }); @@ -866,35 +863,6 @@ export default { APP.UI.initEtherpad(value); }); - room.addCommandListener(Commands.PREZI, ({value, attributes}) => { - APP.UI.showPrezi(attributes.id, value, attributes.slide); - }); - - room.addCommandListener(Commands.STOP_PREZI, ({attributes}) => { - APP.UI.stopPrezi(attributes.id); - }); - - APP.UI.addListener(UIEvents.SHARE_PREZI, (url, slide) => { - console.log('Sharing Prezi %s slide %s', url, slide); - room.removeCommand(Commands.PREZI); - room.sendCommand(Commands.PREZI, { - value: url, - attributes: { - id: room.myUserId(), - slide - } - }); - }); - - APP.UI.addListener(UIEvents.STOP_SHARING_PREZI, () => { - room.removeCommand(Commands.PREZI); - room.sendCommandOnce(Commands.STOP_PREZI, { - attributes: { - id: room.myUserId() - } - }); - }); - APP.UI.addListener(UIEvents.EMAIL_CHANGED, (email = '') => { email = email.trim(); diff --git a/css/font.css b/css/font.css index d7a1d82ed..076d668ac 100644 --- a/css/font.css +++ b/css/font.css @@ -63,9 +63,6 @@ .icon-exit-full-screen:before { content: "\e60e"; } -.icon-prezi:before { - content: "\e60c"; -} .icon-link:before { content: "\e600"; } diff --git a/index.html b/index.html index c476b1547..4154cbdc5 100644 --- a/index.html +++ b/index.html @@ -122,7 +122,6 @@ - @@ -134,7 +133,6 @@
-
diff --git a/interface_config.js b/interface_config.js index aae97d279..7101d6d05 100644 --- a/interface_config.js +++ b/interface_config.js @@ -16,7 +16,7 @@ var interfaceConfig = { INVITATION_POWERED_BY: true, DOMINANT_SPEAKER_AVATAR_SIZE: 100, TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop', - 'recording', 'security', 'invite', 'chat', 'prezi', 'etherpad', + 'recording', 'security', 'invite', 'chat', 'etherpad', 'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip', 'contacts'], // Determines how the video would fit the screen. 'both' would fit the whole diff --git a/lang/main.json b/lang/main.json index 9c5792827..4adb2441f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -9,7 +9,6 @@ "me": "me", "speaker": "Speaker", "defaultNickname": "ex. __name__", - "defaultPreziLink": "e.g. __url__", "welcomepage":{ "go": "GO", "roomname": "Enter room name", @@ -55,7 +54,6 @@ "lock": "Lock / unlock room", "invite": "Invite others", "chat": "Open / close chat", - "prezi": "Share Prezi", "etherpad": "Shared document", "sharescreen": "Share screen", "fullscreen": "Enter / Exit Full Screen", @@ -159,10 +157,6 @@ "defaultError": "There was some kind of error", "passwordRequired": "Password required", "Ok": "Ok", - "removePreziTitle": "Remove Prezi", - "removePreziMsg": "Are you sure you would like to remove your Prezi?", - "sharePreziTitle": "Share a Prezi", - "sharePreziMsg": "Another participant is already sharing a Prezi. This conference allows only one Prezi at a time.", "Remove": "Remove", "WaitingForHost": "Waiting for the host ...", "WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", @@ -175,7 +169,6 @@ "hungUp": "You hung up", "joinAgain": "Join again", "Share": "Share", - "preziLinkError": "Please provide a correct prezi link.", "Save": "Save", "recordingToken": "Enter recording token", "Dial": "Dial", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index e68113ff2..5aef40308 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -12,7 +12,6 @@ import PanelToggler from "./side_pannels/SidePanelToggler"; import UIUtil from "./util/UIUtil"; import UIEvents from "../../service/UI/UIEvents"; import CQEvents from '../../service/connectionquality/CQEvents'; -import PreziManager from './prezi/Prezi'; import EtherpadManager from './etherpad/Etherpad'; import VideoLayout from "./videolayout/VideoLayout"; @@ -30,7 +29,6 @@ var Feedback = require("./Feedback"); var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; -let preziManager; let etherpadManager; /** @@ -96,7 +94,6 @@ function setupChat() { */ function setupToolbars() { Toolbar.init(eventEmitter); - Toolbar.setupButtonsFromConfig(); BottomToolbar.setupListeners(eventEmitter); } @@ -256,9 +253,6 @@ UI.mucJoined = function () { * Setup some UI event listeners. */ function registerListeners() { - UI.addListener(UIEvents.PREZI_CLICKED, function () { - preziManager.handlePreziButtonClicked(); - }); UI.addListener(UIEvents.ETHERPAD_CLICKED, function () { if (etherpadManager) { @@ -337,7 +331,6 @@ UI.start = function () { ContactList.init(eventEmitter); bindEvents(); - preziManager = new PreziManager(eventEmitter); if (!interfaceConfig.filmStripOnly) { $("#videospace").mousemove(function () { @@ -998,26 +991,6 @@ UI.updateAuthInfo = function (isAuthEnabled, login) { } }; -/** - * Show Prezi from the user. - * @param {string} userId user id - * @param {string} url Prezi url - * @param {number} slide slide to show - */ -UI.showPrezi = function (userId, url, slide) { - preziManager.showPrezi(userId, url, slide); -}; - -/** - * Stop showing Prezi from the user. - * @param {string} userId user id - */ -UI.stopPrezi = function (userId) { - if (preziManager.isSharing(userId)) { - preziManager.removePrezi(userId); - } -}; - UI.onStartMutedChanged = function (startAudioMuted, startVideoMuted) { SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted); }; diff --git a/modules/UI/prezi/Prezi.js b/modules/UI/prezi/Prezi.js deleted file mode 100644 index da2b41cb5..000000000 --- a/modules/UI/prezi/Prezi.js +++ /dev/null @@ -1,448 +0,0 @@ -/* global $, APP */ -/* jshint -W101 */ - -import VideoLayout from "../videolayout/VideoLayout"; -import LargeContainer from '../videolayout/LargeContainer'; -import PreziPlayer from './PreziPlayer'; -import UIUtil from '../util/UIUtil'; -import UIEvents from '../../../service/UI/UIEvents'; -import messageHandler from '../util/MessageHandler'; -import ToolbarToggler from "../toolbars/ToolbarToggler"; -import SidePanelToggler from "../side_pannels/SidePanelToggler"; -import FilmStrip from '../videolayout/FilmStrip'; - -/** - * Example of Prezi link. - */ -const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi"; -const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i; -/** - * Default aspect ratio for Prezi frame. - */ -const aspectRatio = 16.0 / 9.0; - -/** - * Default Prezi frame width. - */ -const DEFAULT_WIDTH = 640; -/** - * Default Prezi frame height. - */ -const DEFAULT_HEIGHT = 480; - -/** - * Indicates if the given string is an alphanumeric string. - * Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the - * purpose of checking URIs. - * @param {string} unsafeText string to check - * @returns {boolean} - */ -function isAlphanumeric(unsafeText) { - return alphanumRegex.test(unsafeText); -} - -/** - * Returns the presentation id from the given url. - * @param {string} url Prezi link - * @returns {string} presentation id - */ -function getPresentationId (url) { - let presId = url.substring(url.indexOf("prezi.com/") + 10); - return presId.substring(0, presId.indexOf('/')); -} - -/** - * Checks if given string is Prezi url. - * @param {string} url string to check. - * @returns {boolean} - */ -function isPreziLink(url) { - if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) { - return false; - } - - let presId = url.substring(url.indexOf("prezi.com/") + 10); - if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) { - return false; - } - - return true; -} - -/** - * Notify user that other user if already sharing Prezi. - */ -function notifyOtherIsSharingPrezi() { - messageHandler.openMessageDialog( - "dialog.sharePreziTitle", - "dialog.sharePreziMsg" - ); -} - -/** - * Ask user if he want to close Prezi he's sharing. - */ -function proposeToClosePrezi() { - return new Promise(function (resolve, reject) { - messageHandler.openTwoButtonDialog( - "dialog.removePreziTitle", - null, - "dialog.removePreziMsg", - null, - false, - "dialog.Remove", - function(e,v,m,f) { - if (v) { - resolve(); - } else { - reject(); - } - } - ); - - }); -} - -/** - * Ask user for Prezi url to share with others. - * Dialog validates client input to allow only Prezi urls. - */ -function requestPreziLink() { - const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle"); - const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel"); - const shareButton = APP.translation.generateTranslationHTML("dialog.Share"); - const backButton = APP.translation.generateTranslationHTML("dialog.Back"); - const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError"); - const i18nOptions = {url: defaultPreziLink}; - const defaultUrl = APP.translation.translateString( - "defaultPreziLink", i18nOptions - ); - - return new Promise(function (resolve, reject) { - let dialog = messageHandler.openDialogWithStates({ - state0: { - html: ` -

${title}

- `, - persistent: false, - buttons: [ - {title: cancelButton, value: false}, - {title: shareButton, value: true} - ], - focus: ':input:first', - defaultButton: 1, - submit: function (e, v, m, f) { - e.preventDefault(); - if (!v) { - reject('cancelled'); - dialog.close(); - return; - } - - let preziUrl = f.preziUrl; - if (!preziUrl) { - return; - } - - let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl)); - - if (!isPreziLink(urlValue)) { - dialog.goToState('state1'); - return false; - } - - resolve(urlValue); - dialog.close(); - } - }, - - state1: { - html: `

${title}

${linkError}`, - persistent: false, - buttons: [ - {title: cancelButton, value: false}, - {title: backButton, value: true} - ], - focus: ':input:first', - defaultButton: 1, - submit: function (e, v, m, f) { - e.preventDefault(); - if (v === 0) { - reject(); - dialog.close(); - } else { - dialog.goToState('state0'); - } - } - } - }); - - }); -} - -export const PreziContainerType = "prezi"; - -/** - * Container for Prezi iframe. - */ -class PreziContainer extends LargeContainer { - - constructor ({preziId, isMy, slide, onSlideChanged}) { - super(); - this.reloadBtn = $('#reloadPresentation'); - - let preziPlayer = new PreziPlayer( - 'presentation', { - preziId, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - controls: isMy, - debug: true - } - ); - this.preziPlayer = preziPlayer; - this.$iframe = $(preziPlayer.iframe); - - this.$iframe.attr('id', preziId); - - preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) { - console.log("prezi status", value); - if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) { - preziPlayer.flyToStep(slide); - } - }); - - preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) { - console.log("event value", value); - onSlideChanged(value); - }); - } - - /** - * Change Prezi slide. - * @param {number} slide slide to show - */ - goToSlide (slide) { - if (this.preziPlayer.getCurrentStep() === slide) { - return; - } - - this.preziPlayer.flyToStep(slide); - - let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps(); - if (!animationStepsArray) { - return; - } - - for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) { - this.preziPlayer.flyToStep(slide, i); - } - } - - /** - * Show or hide "reload presentation" button. - * @param {boolean} show - */ - showReloadBtn (show) { - this.reloadBtn.css('display', show ? 'inline-block' : 'none'); - } - - show () { - return new Promise(resolve => { - this.$iframe.fadeIn(300, () => { - this.$iframe.css({opacity: 1}); - ToolbarToggler.dockToolbar(true); - resolve(); - }); - }); - } - - hide () { - return new Promise(resolve => { - this.$iframe.fadeOut(300, () => { - this.$iframe.css({opacity: 0}); - this.showReloadBtn(false); - ToolbarToggler.dockToolbar(false); - resolve(); - }); - }); - } - - onHoverIn () { - let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width(); - - this.showReloadBtn(true); - this.reloadBtn.css('right', rightOffset); - } - - onHoverOut (event) { - let e = event.toElement || event.relatedTarget; - - if (e && e.id != 'reloadPresentation' && e.id != 'header') { - this.showReloadBtn(false); - } - } - - resize (containerWidth, containerHeight) { - let height = containerHeight - FilmStrip.getFilmStripHeight(); - - let width = containerWidth; - - if (height < width / aspectRatio) { - width = Math.floor(height * aspectRatio); - } - - this.$iframe.width(width).height(height); - } - - /** - * Close Prezi frame. - */ - close () { - this.showReloadBtn(false); - this.preziPlayer.destroy(); - this.$iframe.remove(); - } -} - -/** - * Manager of Prezi frames. - */ -export default class PreziManager { - constructor (emitter) { - this.emitter = emitter; - - this.userId = null; - this.url = null; - this.prezi = null; - - $("#reloadPresentationLink").click(this.reloadPresentation.bind(this)); - } - - get isPresenting () { - return !!this.userId; - } - - get isMyPrezi () { - return this.userId === APP.conference.localId; - } - - /** - * Check if user is currently sharing. - * @param {string} id user id to check for - */ - isSharing (id) { - return this.userId === id; - } - - handlePreziButtonClicked () { - if (!this.isPresenting) { - requestPreziLink().then( - url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0), - err => console.error('PREZI CANCELED', err) - ); - return; - } - - if (this.isMyPrezi) { - proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI)); - } else { - notifyOtherIsSharingPrezi(); - } - } - - /** - * Reload current Prezi frame. - */ - reloadPresentation () { - if (!this.prezi) { - return; - } - let iframe = this.prezi.$iframe[0]; - iframe.src = iframe.src; - } - - /** - * Show Prezi. Create new Prezi if there is no Prezi yet. - * @param {string} id owner id - * @param {string} url Prezi url - * @param {number} slide slide to show - */ - showPrezi (id, url, slide) { - if (!this.isPresenting) { - this.createPrezi(id, url, slide); - } - - if (this.userId === id && this.url === url) { - this.prezi.goToSlide(slide); - } else { - console.error(this.userId, id); - console.error(this.url, url); - throw new Error("unexpected presentation change"); - } - } - - /** - * Create new Prezi frame.. - * @param {string} id owner id - * @param {string} url Prezi url - * @param {number} slide slide to show - */ - createPrezi (id, url, slide) { - console.log("presentation added", url); - - this.userId = id; - this.url = url; - - let preziId = getPresentationId(url); - let elementId = `participant_${id}_${preziId}`; - - this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId)); - VideoLayout.resizeThumbnails(); - this.$thumb.css({ - 'background-image': 'url(../images/avatarprezi.png)' - }).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true)); - - this.prezi = new PreziContainer({ - preziId, - isMy: this.isMyPrezi, - slide, - onSlideChanged: newSlide => { - if (this.isMyPrezi) { - this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide); - } - } - }); - - VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi); - VideoLayout.showLargeVideoContainer(PreziContainerType, true); - } - - /** - * Close Prezi. - * @param {string} id owner id - */ - removePrezi (id) { - if (this.userId !== id) { - throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`); - } - - this.$thumb.remove(); - this.$thumb = null; - - // wait until Prezi is hidden, then remove it - VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => { - console.log("presentation removed", this.url); - - VideoLayout.removeLargeVideoContainer(PreziContainerType); - - this.userId = null; - this.url = null; - this.prezi.close(); - this.prezi = null; - }); - } -} diff --git a/modules/UI/prezi/PreziPlayer.js b/modules/UI/prezi/PreziPlayer.js deleted file mode 100644 index b962057ac..000000000 --- a/modules/UI/prezi/PreziPlayer.js +++ /dev/null @@ -1,290 +0,0 @@ -/* jshint -W101 */ - -var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - -PreziPlayer.API_VERSION = 1; -PreziPlayer.CURRENT_STEP = 'currentStep'; -PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep'; -PreziPlayer.CURRENT_OBJECT = 'currentObject'; -PreziPlayer.STATUS_LOADING = 'loading'; -PreziPlayer.STATUS_READY = 'ready'; -PreziPlayer.STATUS_CONTENT_READY = 'contentready'; -PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange"; -PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange"; -PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange"; -PreziPlayer.EVENT_STATUS = "statusChange"; -PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange"; -PreziPlayer.EVENT_IS_MOVING = "isMovingChange"; -PreziPlayer.domain = "https://prezi.com"; -PreziPlayer.path = "/player/"; -PreziPlayer.players = {}; -PreziPlayer.binded_methods = ['changesHandler']; - -PreziPlayer.createMultiplePlayers = function(optionArray){ - for(var i=0; i 0 && - obj.values.animationCountOnSteps && - obj.values.animationCountOnSteps[step] <= animation_step) { - animation_step = obj.values.animationCountOnSteps[step]; - } - // jump to animation steps by calling flyToNextStep() - function doAnimationSteps() { - if (obj.values.isMoving) { - setTimeout(doAnimationSteps, 100); // wait until the flight ends - return; - } - while (animation_step-- > 0) { - obj.flyToNextStep(); // do the animation steps - } - } - setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time - // jump to the step - return this.sendMessage({ - 'action': 'present', - 'data': ['moveToStep', step] - }); -}; - -PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */ -PreziPlayer.prototype.flyToObject = function(objectId) { - return this.sendMessage({ - 'action': 'present', - 'data': ['moveToObject', objectId] - }); -}; - -PreziPlayer.prototype.play = function(defaultDelay) { - return this.sendMessage({ - 'action': 'present', - 'data': ['startAutoPlay', defaultDelay] - }); -}; - -PreziPlayer.prototype.stop = function() { - return this.sendMessage({ - 'action': 'present', - 'data': ['stopAutoPlay'] - }); -}; - -PreziPlayer.prototype.pause = function(defaultDelay) { - return this.sendMessage({ - 'action': 'present', - 'data': ['pauseAutoPlay', defaultDelay] - }); -}; - -PreziPlayer.prototype.getCurrentStep = function() { - return this.values.currentStep; -}; - -PreziPlayer.prototype.getCurrentAnimationStep = function() { - return this.values.currentAnimationStep; -}; - -PreziPlayer.prototype.getCurrentObject = function() { - return this.values.currentObject; -}; - -PreziPlayer.prototype.getStatus = function() { - return this.values.status; -}; - -PreziPlayer.prototype.isPlaying = function() { - return this.values.isAutoPlaying; -}; - -PreziPlayer.prototype.getStepCount = function() { - return this.values.stepCount; -}; - -PreziPlayer.prototype.getAnimationCountOnSteps = function() { - return this.values.animationCountOnSteps; -}; - -PreziPlayer.prototype.getTitle = function() { - return this.values.title; -}; - -PreziPlayer.prototype.setDimensions = function(dims) { - for (var parameter in dims) { - this.iframe[parameter] = dims[parameter]; - } -}; - -PreziPlayer.prototype.getDimensions = function() { - return { - width: parseInt(this.iframe.width, 10), - height: parseInt(this.iframe.height, 10) - }; -}; - -PreziPlayer.prototype.on = function(event, callback) { - this.callbacks.push({ - event: event, - callback: callback - }); -}; - -PreziPlayer.prototype.off = function(event, callback) { - var j, item; - if (event === undefined) { - this.callbacks = []; - } - j = this.callbacks.length; - while (j--) { - item = this.callbacks[j]; - if (item && item.event === event && (callback === undefined || item.callback === callback)){ - this.callbacks.splice(j, 1); - } - } -}; - -if (window.addEventListener) { - window.addEventListener('message', PreziPlayer.messageReceived, false); -} else { - window.attachEvent('onmessage', PreziPlayer.messageReceived); -} - -window.PreziPlayer = PreziPlayer; - -export default PreziPlayer; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index e2b79b5bf..992af2d22 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -122,10 +122,6 @@ const buttonHandlers = { AnalyticsAdapter.sendEvent('toolbar.chat.toggled'); emitter.emit(UIEvents.TOGGLE_CHAT); }, - "toolbar_button_prezi": function () { - AnalyticsAdapter.sendEvent('toolbar.prezi.clicked'); - emitter.emit(UIEvents.PREZI_CLICKED); - }, "toolbar_button_etherpad": function () { AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked'); emitter.emit(UIEvents.ETHERPAD_CLICKED); @@ -188,7 +184,6 @@ const defaultToolbarButtons = { 'security': '#toolbar_button_security', 'invite': '#toolbar_button_link', 'chat': '#toolbar_button_chat', - 'prezi': '#toolbar_button_prezi', 'etherpad': '#toolbar_button_etherpad', 'fullscreen': '#toolbar_button_fullScreen', 'settings': '#toolbar_button_settings', @@ -246,15 +241,6 @@ const Toolbar = { } }, - /** - * Disables and enables some of the buttons. - */ - setupButtonsFromConfig () { - if (!UIUtil.isButtonEnabled('prezi')) { - $("#toolbar_button_prezi").css({display: "none"}); - } - }, - /** * Unlocks the lock button state. */ diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index b31589e7b..f34c2544b 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -124,9 +124,7 @@ isButtonEnabled: function (name) { var isEnabled = interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1; - if (name === 'prezi') { - return isEnabled && !config.disablePrezi; - } else if (name === 'recording') { + if (name === 'recording') { return isEnabled && config.enableRecording; } return isEnabled; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 730a55dc3..e1550503e 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -10,7 +10,6 @@ import UIUtil from "../util/UIUtil"; import RemoteVideo from "./RemoteVideo"; import LargeVideoManager, {VideoContainerType} from "./LargeVideo"; -import {PreziContainerType} from '../prezi/Prezi'; import LocalVideo from "./LocalVideo"; import PanelToggler from "../side_pannels/SidePanelToggler"; @@ -361,11 +360,10 @@ var VideoLayout = { false, false, false, function() {$(videoelem).show();}); // Update the large video to the last added video only if there's no - // current dominant, focused speaker or prezi playing or update it to + // current dominant, focused speaker or update it to // the current dominant speaker. if ((!focusedVideoResourceJid && - !currentDominantSpeaker && - !this.isLargeContainerTypeVisible(PreziContainerType)) || + !currentDominantSpeaker) || focusedVideoResourceJid === resourceJid || (resourceJid && currentDominantSpeaker === resourceJid)) { @@ -793,10 +791,6 @@ var VideoLayout = { } }, - addRemoteVideoContainer (id) { - return RemoteVideo.createContainer(id); - }, - /** * Resizes the video area. * diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index b915b3793..cc507164c 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -20,10 +20,6 @@ export default { START_MUTED_CHANGED: "UI.start_muted_changed", AUDIO_MUTED: "UI.audio_muted", VIDEO_MUTED: "UI.video_muted", - PREZI_CLICKED: "UI.prezi_clicked", - SHARE_PREZI: "UI.share_prezi", - PREZI_SLIDE_CHANGED: "UI.prezi_slide_changed", - STOP_SHARING_PREZI: "UI.stop_sharing_prezi", ETHERPAD_CLICKED: "UI.etherpad_clicked", ROOM_LOCK_CLICKED: "UI.room_lock_clicked", USER_INVITED: "UI.user_invited", From 97733cd17ae6ef1c2a0c499c51d0cf032d48d607 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 11 Mar 2016 12:57:49 -0600 Subject: [PATCH 05/51] Resizes large video on filmstrip toggle. --- modules/UI/UI.js | 5 ++++- modules/UI/videolayout/VideoLayout.js | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index e68113ff2..60c453200 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -276,7 +276,10 @@ function registerListeners() { UI.addListener(UIEvents.TOGGLE_CONTACT_LIST, UI.toggleContactList); - UI.addListener(UIEvents.TOGGLE_FILM_STRIP, UI.toggleFilmStrip); + UI.addListener(UIEvents.TOGGLE_FILM_STRIP, function () { + UI.toggleFilmStrip(); + VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, true); + }); } /** diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 730a55dc3..a671c67b6 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -804,10 +804,11 @@ var VideoLayout = { * @param forceUpdate indicates that hidden thumbnails will be shown * @param completeFunction a function to be called when the video area is * resized. - */resizeVideoArea (isSideBarVisible, - forceUpdate = false, - animate = false, - completeFunction = null) { + */ + resizeVideoArea (isSideBarVisible, + forceUpdate = false, + animate = false, + completeFunction = null) { if (largeVideo) { largeVideo.updateContainerSize(isSideBarVisible); From 02ff54b6599cf3e90639460b9f74de42965e9fb5 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Tue, 15 Mar 2016 14:08:01 -0500 Subject: [PATCH 06/51] Implements dialog for max users error --- conference.js | 4 ++++ lang/main.json | 1 + modules/UI/UI.js | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/conference.js b/conference.js index 529a6c1aa..9d5813164 100644 --- a/conference.js +++ b/conference.js @@ -258,6 +258,10 @@ class ConferenceConnector { APP.UI.notifyFocusLeft(); break; + case ConferenceErrors.CONFERENCE_MAX_USERS: + connection.disconnect(); + APP.UI.notifyMaxUsersLimitReached(); + break; default: this._handleConferenceFailed(err, ...params); } diff --git a/lang/main.json b/lang/main.json index 9c5792827..f9128e92d 100644 --- a/lang/main.json +++ b/lang/main.json @@ -146,6 +146,7 @@ "failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.", "bridgeUnavailable": "Jitsi Videobridge is currently unavailable. Please try again later!", "jicofoUnavailable": "Jicofo is currently unavailable. Please try again later!", + "maxUsersLimitReached": "The limit for maximum number of participants in the conference is already reached. The conference is full. Please try again later!", "lockTitle": "Lock failed", "lockMessage": "Failed to lock the conference.", "warning": "Warning", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index e68113ff2..70644a2b1 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -732,6 +732,22 @@ UI.notifyConnectionFailed = function (stropheErrorMsg) { ); }; + +/** + * Notify user that max users limit is already reached. + */ +UI.notifyMaxUsersLimitReached = function () { + var title = APP.translation.generateTranslationHTML( + "dialog.error"); + + var message = APP.translation.generateTranslationHTML( + "dialog.maxUsersLimitReached"); + + messageHandler.openDialog( + title, message, true, {}, function (e, v, m, f) { return false; } + ); +}; + /** * Notify user that he was automatically muted when joned the conference. */ From 3fc839cb374de5055452cf9c248fc6f5d3e7fba5 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 15 Mar 2016 13:18:57 -0500 Subject: [PATCH 07/51] Adds optional parameter to addParticipantContainer, make possible to create the SmallVideo outside VideoLayout and pass it to be displayed. --- modules/UI/videolayout/VideoLayout.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index e1550503e..787835b55 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -326,16 +326,18 @@ var VideoLayout = { this.updateLargeVideo(resourceJid); }, - /** - * Checks if container for participant identified by given id exists - * in the document and creates it eventually. - * - * @return Returns true if the peer container exists, - * false - otherwise + * Creates a remote video for participant for the given id. + * @param id the id of the participant to add + * @param {SmallVideo} smallVideo optional small video instance to add as a + * remote video, if undefined RemoteVideo will be created */ - addParticipantContainer (id) { - let remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter); + addParticipantContainer (id, smallVideo) { + let remoteVideo; + if(smallVideo) + remoteVideo = smallVideo; + else + remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter); remoteVideos[id] = remoteVideo; let videoType = remoteVideoTypes[id]; From 5837ef506c2046429a8963c5a4c6b094f5c8b7eb Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 15 Mar 2016 13:36:17 -0500 Subject: [PATCH 08/51] Removes remoteVideoTypes and uses the set/get in the small video instances. --- modules/UI/videolayout/VideoLayout.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 787835b55..3a6e7c2fe 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -16,7 +16,6 @@ import PanelToggler from "../side_pannels/SidePanelToggler"; const RTCUIUtil = JitsiMeetJS.util.RTCUIHelper; var remoteVideos = {}; -var remoteVideoTypes = {}; var localVideoThumbnail = null; var currentDominantSpeaker = null; @@ -277,10 +276,11 @@ var VideoLayout = { /** * Return the type of the remote video. * @param id the id for the remote video - * @returns the video type video or screen. + * @returns {String} the video type video or screen. */ getRemoteVideoType (id) { - return remoteVideoTypes[id]; + let smallVideo = VideoLayout.getSmallVideo(id); + return smallVideo ? smallVideo.getVideoType() : null; }, handleVideoThumbClicked (noPinnedEndpointChangedEvent, @@ -340,10 +340,12 @@ var VideoLayout = { remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter); remoteVideos[id] = remoteVideo; - let videoType = remoteVideoTypes[id]; - if (videoType) { - remoteVideo.setVideoType(videoType); + let videoType = VideoLayout.getRemoteVideoType(id); + if (!videoType) { + // make video type the default one (camera) + videoType = VideoContainerType; } + remoteVideo.setVideoType(videoType); // In case this is not currently in the last n we don't show it. if (localLastNCount && localLastNCount > 0 && @@ -754,12 +756,11 @@ var VideoLayout = { }, onVideoTypeChanged (id, newVideoType) { - if (remoteVideoTypes[id] === newVideoType) { + if (VideoLayout.getRemoteVideoType(id) === newVideoType) { return; } console.info("Peer video type changed: ", id, newVideoType); - remoteVideoTypes[id] = newVideoType; var smallVideo; if (APP.conference.isLocalId(id)) { @@ -773,8 +774,8 @@ var VideoLayout = { } else { return; } - smallVideo.setVideoType(newVideoType); + if (this.isCurrentlyOnLarge(id)) { this.updateLargeVideo(id, true); } From 6955bb71f3a923d14392e16add15824ec7d50fac Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 15 Mar 2016 15:38:10 -0500 Subject: [PATCH 09/51] Adds methods to abstraction LargeContainer which are used by LargeVideo when updating video. --- modules/UI/videolayout/LargeContainer.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/UI/videolayout/LargeContainer.js b/modules/UI/videolayout/LargeContainer.js index 319ccef62..cc80f3f42 100644 --- a/modules/UI/videolayout/LargeContainer.js +++ b/modules/UI/videolayout/LargeContainer.js @@ -38,4 +38,20 @@ export default class LargeContainer { */ onHoverOut (e) { } + + /** + * Update video stream. + * @param {JitsiTrack?} stream new stream + * @param {string} videoType video type + */ + setStream (stream, videoType) { + } + + /** + * Show or hide user avatar. + * @param {boolean} show + */ + showAvatar (show) { + } + } From ca56734d9cc735e6cbfe7043f02deb7bf34e7870 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 15 Mar 2016 15:42:53 -0500 Subject: [PATCH 10/51] Moves VideoLayout reference in SmallVideo as it is used there in updateView. --- modules/UI/videolayout/LocalVideo.js | 3 +-- modules/UI/videolayout/RemoteVideo.js | 3 +-- modules/UI/videolayout/SmallVideo.js | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index c108e3252..380e9d7cc 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -13,7 +13,6 @@ function LocalVideo(VideoLayout, emitter) { this.videoSpanId = "localVideoContainer"; this.container = $("#localVideoContainer").get(0); this.bindHoverHandler(); - this.VideoLayout = VideoLayout; this.flipX = true; this.isLocal = true; this.emitter = emitter; @@ -22,7 +21,7 @@ function LocalVideo(VideoLayout, emitter) { return APP.conference.localId; } }); - SmallVideo.call(this); + SmallVideo.call(this, VideoLayout); } LocalVideo.prototype = Object.create(SmallVideo.prototype); diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index d9f4dae10..60c9fbcf9 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -11,14 +11,13 @@ function RemoteVideo(id, VideoLayout, emitter) { this.id = id; this.emitter = emitter; this.videoSpanId = `participant_${id}`; - this.VideoLayout = VideoLayout; + SmallVideo.call(this, VideoLayout); this.addRemoteVideoContainer(); this.connectionIndicator = new ConnectionIndicator(this, id); this.setDisplayName(); this.bindHoverHandler(); this.flipX = false; this.isLocal = false; - SmallVideo.call(this); } RemoteVideo.prototype = Object.create(SmallVideo.prototype); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index e55eafec6..fa5bac0bf 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -5,12 +5,13 @@ import UIUtil from "../util/UIUtil"; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; -function SmallVideo() { +function SmallVideo(VideoLayout) { this.isMuted = false; this.hasAvatar = false; this.isVideoMuted = false; this.videoStream = null; this.audioStream = null; + this.VideoLayout = VideoLayout; } function setVisibility(selector, show) { From 9e0b1beed555c8ac7ec85e2d34806009a77337e5 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 15 Mar 2016 16:03:38 -0500 Subject: [PATCH 11/51] Always uses current video type to search for container that will handle it. Renames video container type to camera, to match the type ot the tracks. Add the same container to be used and for tracks with type of desktop. --- modules/UI/videolayout/LargeVideo.js | 19 ++++++++++++------- modules/UI/videolayout/VideoLayout.js | 9 +++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 9f27bb8b2..7d755f670 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -150,7 +150,7 @@ function getDesktopVideoPosition(videoWidth, return { horizontalIndent, verticalIndent }; } -export const VideoContainerType = "video"; +export const VideoContainerType = "camera"; /** * Container for user video. @@ -357,6 +357,8 @@ export default class LargeVideoManager { this.state = VideoContainerType; this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType)); this.addContainer(VideoContainerType, this.videoContainer); + // use the same video container to handle and desktop tracks + this.addContainer("desktop", this.videoContainer); this.width = 0; this.height = 0; @@ -413,7 +415,8 @@ export default class LargeVideoManager { } get id () { - return this.videoContainer.id; + let container = this.getContainer(this.state); + return container.id; } scheduleLargeVideoUpdate () { @@ -430,8 +433,9 @@ export default class LargeVideoManager { this.newStreamData = null; console.info("hover in %s", id); - this.state = VideoContainerType; - this.videoContainer.setStream(stream, videoType); + this.state = videoType; + let container = this.getContainer(this.state); + container.setStream(stream, videoType); // change the avatar url on large this.updateAvatar(Avatar.getAvatarUrl(id)); @@ -439,7 +443,7 @@ export default class LargeVideoManager { let isVideoMuted = stream ? stream.isMuted() : true; // show the avatar on large if needed - this.videoContainer.showAvatar(isVideoMuted); + container.showAvatar(isVideoMuted); let promise; @@ -449,7 +453,7 @@ export default class LargeVideoManager { this.showWatermark(true); promise = Promise.resolve(); } else { - promise = this.videoContainer.show(); + promise = container.show(); } // resolve updateLargeVideo promise after everything is done @@ -529,7 +533,8 @@ export default class LargeVideoManager { * @param enable true to enable, false to disable */ enableVideoProblemFilter (enable) { - this.videoContainer.$video.toggleClass("videoProblemFilter", enable); + let container = this.getContainer(this.state); + container.$video.toggleClass("videoProblemFilter", enable); } /** diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 3a6e7c2fe..31adc8ae1 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -801,10 +801,11 @@ var VideoLayout = { * @param forceUpdate indicates that hidden thumbnails will be shown * @param completeFunction a function to be called when the video area is * resized. - */resizeVideoArea (isSideBarVisible, - forceUpdate = false, - animate = false, - completeFunction = null) { + */ + resizeVideoArea (isSideBarVisible, + forceUpdate = false, + animate = false, + completeFunction = null) { if (largeVideo) { largeVideo.updateContainerSize(isSideBarVisible); From 3577f338ccbd748f3d96de0a8cfbb8e0f94265c0 Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 16 Mar 2016 14:35:30 -0500 Subject: [PATCH 12/51] Does not do force-update when resizing. --- modules/UI/UI.js | 2 +- modules/UI/side_pannels/SidePanelToggler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 60c453200..880e017b4 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -278,7 +278,7 @@ function registerListeners() { UI.addListener(UIEvents.TOGGLE_FILM_STRIP, function () { UI.toggleFilmStrip(); - VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, true); + VideoLayout.resizeVideoArea(PanelToggler.isVisible(), true, false); }); } diff --git a/modules/UI/side_pannels/SidePanelToggler.js b/modules/UI/side_pannels/SidePanelToggler.js index 9336d65b1..8d4d6f47b 100644 --- a/modules/UI/side_pannels/SidePanelToggler.js +++ b/modules/UI/side_pannels/SidePanelToggler.js @@ -93,7 +93,7 @@ function toggle (object, selector, onOpenComplete, function resizeVideoArea(isSidePanelVisible, completeFunction) { VideoLayout.resizeVideoArea(!isSidePanelVisible, false, - true, + false, completeFunction); } From 438cae101f25838cad3e54b25c11e598d13ad4da Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Thu, 17 Mar 2016 13:23:03 -0500 Subject: [PATCH 13/51] Fixes error with max users limit after review --- lang/main.json | 2 +- modules/UI/UI.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/main.json b/lang/main.json index f9128e92d..d26735edf 100644 --- a/lang/main.json +++ b/lang/main.json @@ -146,7 +146,7 @@ "failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.", "bridgeUnavailable": "Jitsi Videobridge is currently unavailable. Please try again later!", "jicofoUnavailable": "Jicofo is currently unavailable. Please try again later!", - "maxUsersLimitReached": "The limit for maximum number of participants in the conference is already reached. The conference is full. Please try again later!", + "maxUsersLimitReached": "The limit for maximum number of participants in the conference has been reached. The conference is full. Please try again later!", "lockTitle": "Lock failed", "lockMessage": "Failed to lock the conference.", "warning": "Warning", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 70644a2b1..59d282931 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -734,7 +734,7 @@ UI.notifyConnectionFailed = function (stropheErrorMsg) { /** - * Notify user that max users limit is already reached. + * Notify user that maximum users limit has been reached. */ UI.notifyMaxUsersLimitReached = function () { var title = APP.translation.generateTranslationHTML( From 5963f85ad6f1799c9f38e1deb70b1dc65eadecc6 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 17 Mar 2016 13:29:07 -0500 Subject: [PATCH 14/51] Fixes avatar that is shown, when we switch from container with muted video to container from other type, like etherpad. --- modules/UI/videolayout/LargeVideo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 7d755f670..88d74cd65 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -332,6 +332,10 @@ class VideoContainer extends LargeContainer { } hide () { + // as the container is hidden/replaced by another container + // hide its avatar + this.showAvatar(false); + // its already hidden if (!this.isVisible) { return Promise.resolve(); From eefdbd4fe59589b369d9d68abf39a1048278db8f Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 17 Mar 2016 21:58:40 -0500 Subject: [PATCH 15/51] Renames EtherpadContainerType to ETHERPAD_CONTAINER_TYPE. --- modules/UI/etherpad/Etherpad.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js index f5f58f921..b7feff895 100644 --- a/modules/UI/etherpad/Etherpad.js +++ b/modules/UI/etherpad/Etherpad.js @@ -52,7 +52,7 @@ const DEFAULT_WIDTH = 640; */ const DEFAULT_HEIGHT = 480; -const EtherpadContainerType = "etherpad"; +const ETHERPAD_CONTAINER_TYPE = "etherpad"; /** * Container for Etherpad iframe. @@ -159,7 +159,7 @@ export default class EtherpadManager { openEtherpad () { this.etherpad = new Etherpad(this.domain, this.name); VideoLayout.addLargeVideoContainer( - EtherpadContainerType, + ETHERPAD_CONTAINER_TYPE, this.etherpad ); } @@ -174,9 +174,10 @@ export default class EtherpadManager { } let isVisible = VideoLayout.isLargeContainerTypeVisible( - EtherpadContainerType + ETHERPAD_CONTAINER_TYPE ); - VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible); + VideoLayout.showLargeVideoContainer( + ETHERPAD_CONTAINER_TYPE, !isVisible); } } From 2b26580a7cc832d014a4a582a6238e3b89e51ef3 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 17 Mar 2016 22:19:09 -0500 Subject: [PATCH 16/51] Disables switching to dominant speaker when Etherpad is shown on large video. --- modules/UI/etherpad/Etherpad.js | 7 +++++++ modules/UI/videolayout/LargeContainer.js | 7 +++++++ modules/UI/videolayout/LargeVideo.js | 7 +++++++ modules/UI/videolayout/VideoLayout.js | 12 +++++++++++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js index b7feff895..01979eb07 100644 --- a/modules/UI/etherpad/Etherpad.js +++ b/modules/UI/etherpad/Etherpad.js @@ -133,6 +133,13 @@ class Etherpad extends LargeContainer { }); }); } + + /** + * @return {boolean} do not switch on dominant speaker event if on stage. + */ + stayOnStage () { + return true; + } } /** diff --git a/modules/UI/videolayout/LargeContainer.js b/modules/UI/videolayout/LargeContainer.js index cc80f3f42..0f0dd242d 100644 --- a/modules/UI/videolayout/LargeContainer.js +++ b/modules/UI/videolayout/LargeContainer.js @@ -54,4 +54,11 @@ export default class LargeContainer { showAvatar (show) { } + /** + * Whether current container needs to be switched on dominant speaker event + * when the container is on stage. + * @return {boolean} + */ + stayOnStage () { + } } diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 88d74cd65..494062af9 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -349,6 +349,13 @@ class VideoContainer extends LargeContainer { }); }); } + + /** + * @return {boolean} switch on dominant speaker event if on stage. + */ + stayOnStage () { + return false; + } } /** diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 31adc8ae1..22e6e16ef 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -524,7 +524,9 @@ 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 && remoteVideo.hasVideoStarted()) { + if (!focusedVideoResourceJid + && remoteVideo.hasVideoStarted() + && !this.getCurrentlyOnLargeContainer().stayOnStage()) { this.updateLargeVideo(id); } }, @@ -889,6 +891,14 @@ var VideoLayout = { return this.isLargeContainerTypeVisible(VideoContainerType); }, + /** + * @return {LargeContainer} the currently displayed container on large + * video. + */ + getCurrentlyOnLargeContainer () { + return largeVideo.getContainer(largeVideo.state); + }, + isCurrentlyOnLarge (id) { return largeVideo && largeVideo.id === id; }, From a61ce8ee3b991c29e93153d55f3a02c002cf26fb Mon Sep 17 00:00:00 2001 From: Etienne CHAMPETIER Date: Fri, 18 Mar 2016 15:22:16 +0100 Subject: [PATCH 17/51] Add unsupported_browser.css to source-package Signed-off-by: Etienne CHAMPETIER --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 735923fbc..12624e6e0 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,6 @@ source-package: mkdir -p source_package/jitsi-meet/css && \ cp -r analytics.js external_api.js favicon.ico fonts images index.html interface_config.js libs plugin.*html sounds title.html unsupported_browser.html LICENSE config.js lang source_package/jitsi-meet && \ cp css/all.css source_package/jitsi-meet/css && \ + cp css/unsupported_browser.css source_package/jitsi-meet/css && \ (cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \ rm -rf source_package From 38275ce045f904299ea28ef02af2802145387be8 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 18 Mar 2016 15:00:55 -0500 Subject: [PATCH 18/51] Shared video, synchronized playing/seek/muting/volume initial commit. --- conference.js | 46 ++- css/main.css | 1 + css/videolayout_default.css | 6 + index.html | 2 + interface_config.js | 2 +- lang/main.json | 4 + modules/UI/UI.js | 47 +++ modules/UI/etherpad/Etherpad.js | 3 + modules/UI/shared_video/SharedVideo.js | 503 +++++++++++++++++++++++++ modules/UI/toolbars/Toolbar.js | 13 + modules/UI/videolayout/LargeVideo.js | 15 +- modules/UI/videolayout/SmallVideo.js | 9 +- modules/UI/videolayout/VideoLayout.js | 31 +- service/UI/UIEvents.js | 7 + 14 files changed, 666 insertions(+), 23 deletions(-) create mode 100644 modules/UI/shared_video/SharedVideo.js diff --git a/conference.js b/conference.js index 57a660156..ad08b8a53 100644 --- a/conference.js +++ b/conference.js @@ -27,7 +27,8 @@ let room, connection, localAudio, localVideo, roomLocker; const Commands = { CONNECTION_QUALITY: "stats", EMAIL: "email", - ETHERPAD: "etherpad" + ETHERPAD: "etherpad", + SHARED_VIDEO: "shared-video" }; /** @@ -689,6 +690,7 @@ export default { console.log('USER %s LEFT', id, user); APP.API.notifyUserLeft(id); APP.UI.removeUser(id, user.getDisplayName()); + APP.UI.stopSharedVideo({from: id}); }); @@ -1012,5 +1014,47 @@ export default { APP.UI.addListener( UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this) ); + + APP.UI.addListener(UIEvents.UPDATE_SHARED_VIDEO, + (url, state, time, volume) => { + // send start and stop commands once, and remove any updates + // that had left + if (state === 'stop' || state === 'start' || state === 'playing') { + room.removeCommand(Commands.SHARED_VIDEO); + room.sendCommandOnce(Commands.SHARED_VIDEO, { + value: url, + attributes: { + from: APP.conference.localId, + state: state, + time: time, + volume: volume + } + }); + } + else { + // in case of paused, in order to allow late users to join + // paused + room.sendCommand(Commands.SHARED_VIDEO, { + value: url, + attributes: { + from: APP.conference.localId, + state: state, + time: time, + volume: volume + } + }); + } + }); + room.addCommandListener( + Commands.SHARED_VIDEO, ({value, attributes}) => { + if (attributes.state === 'stop') { + APP.UI.stopSharedVideo(attributes); + } else if (attributes.state === 'start') { + APP.UI.showSharedVideo(value, attributes); + } else if (attributes.state === 'playing' + || attributes.state === 'pause') { + APP.UI.updateSharedVideo(value, attributes); + } + }); } }; diff --git a/css/main.css b/css/main.css index 5a937025f..a79958423 100644 --- a/css/main.css +++ b/css/main.css @@ -63,6 +63,7 @@ html, body{ text-align: center; text-shadow: 0 1px 0 rgba(255,255,255,.3), 0 -1px 0 rgba(0,0,0,.6); z-index: 1; + font-size: 1.22em !important; } .toolbar_span>span { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 73668cf56..0469afb9c 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -31,6 +31,7 @@ position: relative; margin-left: auto; margin-right: auto; + text-align: center; } #remoteVideos .videocontainer { @@ -112,6 +113,7 @@ } #presentation, +#sharedVideo, #etherpad, #localVideoWrapper>video, #localVideoWrapper>object, @@ -436,6 +438,10 @@ border-radius: 200px; } +.sharedVideoAvatar { + height: 100%; +} + .noMic { position: absolute; border-radius: 8px; diff --git a/index.html b/index.html index 4154cbdc5..6c8a831db 100644 --- a/index.html +++ b/index.html @@ -123,6 +123,7 @@ + @@ -137,6 +138,7 @@
+
diff --git a/interface_config.js b/interface_config.js index 7101d6d05..1f6754bd1 100644 --- a/interface_config.js +++ b/interface_config.js @@ -16,7 +16,7 @@ var interfaceConfig = { INVITATION_POWERED_BY: true, DOMINANT_SPEAKER_AVATAR_SIZE: 100, TOOLBAR_BUTTONS: ['authentication', 'microphone', 'camera', 'desktop', - 'recording', 'security', 'invite', 'chat', 'etherpad', + 'recording', 'security', 'invite', 'chat', 'etherpad', 'sharedvideo', 'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip', 'contacts'], // Determines how the video would fit the screen. 'both' would fit the whole diff --git a/lang/main.json b/lang/main.json index 6eaf7e18e..12d579de8 100644 --- a/lang/main.json +++ b/lang/main.json @@ -9,6 +9,7 @@ "me": "me", "speaker": "Speaker", "defaultNickname": "ex. __name__", + "defaultLink": "e.g. __url__", "welcomepage":{ "go": "GO", "roomname": "Enter room name", @@ -55,6 +56,7 @@ "invite": "Invite others", "chat": "Open / close chat", "etherpad": "Shared document", + "sharedvideo": "Shared video", "sharescreen": "Share screen", "fullscreen": "Enter / Exit Full Screen", "sip": "Call SIP number", @@ -159,6 +161,8 @@ "passwordRequired": "Password required", "Ok": "Ok", "Remove": "Remove", + "shareVideoTitle": "Share a video", + "shareVideoLinkError": "Please provide a correct youtube link.", "WaitingForHost": "Waiting for the host ...", "WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "IamHost": "I am the host", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 8cfa0e067..fcd39c4fc 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -13,6 +13,7 @@ import UIUtil from "./util/UIUtil"; import UIEvents from "../../service/UI/UIEvents"; import CQEvents from '../../service/connectionquality/CQEvents'; import EtherpadManager from './etherpad/Etherpad'; +import SharedVideoManager from './shared_video/SharedVideo'; import VideoLayout from "./videolayout/VideoLayout"; import FilmStrip from "./videolayout/FilmStrip"; @@ -30,6 +31,7 @@ var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; let etherpadManager; +let sharedVideoManager; /** * Prompt user for nickname. @@ -260,6 +262,12 @@ function registerListeners() { } }); + UI.addListener(UIEvents.SHARED_VIDEO_CLICKED, function () { + if (sharedVideoManager) { + sharedVideoManager.toggleSharedVideo(); + } + }); + UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen); UI.addListener(UIEvents.TOGGLE_CHAT, UI.toggleChat); @@ -334,6 +342,7 @@ UI.start = function () { ContactList.init(eventEmitter); bindEvents(); + sharedVideoManager = new SharedVideoManager(eventEmitter); if (!interfaceConfig.filmStripOnly) { $("#videospace").mousemove(function () { @@ -530,6 +539,7 @@ UI.updateLocalRole = function (isModerator) { Toolbar.showSipCallButton(isModerator); Toolbar.showRecordingButton(isModerator); + Toolbar.showSharedVideoButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); if (isModerator) { @@ -1030,6 +1040,14 @@ UI.getLargeVideoID = function () { return VideoLayout.getLargeVideoID(); }; +/** + * Returns the current video shown on large. + * Currently used by tests (torture). + */ +UI.getLargeVideo = function () { + return VideoLayout.getLargeVideo(); +}; + /** * Shows dialog with a link to FF extension. */ @@ -1046,4 +1064,33 @@ UI.updateDevicesAvailability = function (id, devices) { VideoLayout.setDeviceAvailabilityIcons(id, devices); }; +/** +* Show shared video. +* @param {string} url video url +* @param {string} attributes +*/ +UI.showSharedVideo = function (url, attributes) { + if (sharedVideoManager) + sharedVideoManager.showSharedVideo(url, attributes); +}; + +/** + * Update shared video. + * @param {string} url video url + * @param {string} attributes + */ +UI.updateSharedVideo = function (url, attributes) { + if (sharedVideoManager) + sharedVideoManager.updateSharedVideo(url, attributes); +}; + +/** + * Stop showing shared video. + * @param {string} attributes + */ +UI.stopSharedVideo = function (attributes) { + if (sharedVideoManager) + sharedVideoManager.stopSharedVideo(attributes); +}; + module.exports = UI; diff --git a/modules/UI/etherpad/Etherpad.js b/modules/UI/etherpad/Etherpad.js index 01979eb07..7c65016a5 100644 --- a/modules/UI/etherpad/Etherpad.js +++ b/modules/UI/etherpad/Etherpad.js @@ -110,9 +110,11 @@ class Etherpad extends LargeContainer { show () { const $iframe = $(this.iframe); const $container = $(this.container); + let self = this; return new Promise(resolve => { $iframe.fadeIn(300, function () { + self.bodyBackground = document.body.style.background; document.body.style.background = '#eeeeee'; $iframe.css({visibility: 'visible'}); $container.css({zIndex: 2}); @@ -124,6 +126,7 @@ class Etherpad extends LargeContainer { hide () { const $iframe = $(this.iframe); const $container = $(this.container); + document.body.style.background = this.bodyBackground; return new Promise(resolve => { $iframe.fadeOut(300, function () { diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js new file mode 100644 index 000000000..5b7d16620 --- /dev/null +++ b/modules/UI/shared_video/SharedVideo.js @@ -0,0 +1,503 @@ +/* global $, APP, YT, onPlayerReady, onPlayerStateChange, onPlayerError */ + +import messageHandler from '../util/MessageHandler'; +import UIUtil from '../util/UIUtil'; +import UIEvents from '../../../service/UI/UIEvents'; + +import VideoLayout from "../videolayout/VideoLayout"; +import LargeContainer from '../videolayout/LargeContainer'; +import SmallVideo from '../videolayout/SmallVideo'; +import FilmStrip from '../videolayout/FilmStrip'; +import ToolbarToggler from "../toolbars/ToolbarToggler"; + +export const SHARED_VIDEO_CONTAINER_TYPE = "sharedvideo"; + +/** + * Example shared video link. + * @type {string} + */ +const defaultSharedVideoLink = "https://www.youtube.com/watch?v=xNXN7CZk8X0"; + +/** + * Manager of shared video. + */ +export default class SharedVideoManager { + constructor (emitter) { + this.emitter = emitter; + this.isSharedVideoShown = false; + this.isPlayerAPILoaded = false; + this.updateInterval = 5000; // milliseconds + } + + /** + * Starts shared video by asking user for url, or if its already working + * asks whether the user wants to stop sharing the video. + */ + toggleSharedVideo () { + if(!this.isSharedVideoShown) { + requestVideoLink().then( + url => this.emitter.emit( + UIEvents.UPDATE_SHARED_VIDEO, url, 'start'), + err => console.error('SHARED VIDEO CANCELED', err) + ); + return; + } + + this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop'); + } + + /** + * Shows the player component and starts the checking function + * that will be sending updates, if we are the one shared the video + * @param url the video url + * @param attributes + */ + showSharedVideo (url, attributes) { + if (this.isSharedVideoShown) + return; + + // the video url + this.url = url; + + // the owner of the video + this.from = attributes.from; + + // This code loads the IFrame Player API code asynchronously. + var tag = document.createElement('script'); + + tag.src = "https://www.youtube.com/iframe_api"; + var firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + + var self = this; + if(self.isPlayerAPILoaded) + window.onYouTubeIframeAPIReady(); + else + window.onYouTubeIframeAPIReady = function() { + self.isPlayerAPILoaded = true; + let showControls = APP.conference.isLocalId(self.from) ? 1 : 0; + self.player = new YT.Player('sharedVideoIFrame', { + height: '100%', + width: '100%', + videoId: self.url, + playerVars: { + 'origin': location.origin, + 'fs': '0', + 'autoplay': 1, + 'controls': showControls, + 'rel' : 0 + }, + events: { + 'onReady': onPlayerReady, + 'onStateChange': onPlayerStateChange, + 'onError': onPlayerError + } + }); + }; + + window.onPlayerStateChange = function(event) { + if (event.data == YT.PlayerState.PLAYING) { + self.playerPaused = false; + self.updateCheck(); + } else if (event.data == YT.PlayerState.PAUSED) { + self.playerPaused = true; + self.updateCheck(); + } + }; + + window.onPlayerReady = function(event) { + let player = event.target; + player.playVideo(); + + let thumb = new SharedVideoThumb(self.url); + thumb.setDisplayName(player.getVideoData().title); + VideoLayout.addParticipantContainer(self.url, thumb); + + let iframe = player.getIframe(); + self.sharedVideo = new SharedVideoContainer( + {url, iframe, player}); + + VideoLayout.addLargeVideoContainer( + SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo); + VideoLayout.handleVideoThumbClicked(true, self.url); + + self.isSharedVideoShown = true; + + if(APP.conference.isLocalId(self.from)) { + self.intervalId = setInterval( + self.updateCheck.bind(self), + self.updateInterval); + } + + // set initial state + if(attributes.state === 'pause') + player.pauseVideo(); + else if(attributes.time > 0) { + console.log("Player seekTo:", attributes.time); + player.seekTo(attributes.time); + } + }; + + window.onPlayerError = function(event) { + console.error("Error in the player:" + event.data); + }; + } + + /** + * Checks current state of the player and fire an event with the values. + */ + updateCheck() + { + // ignore update checks if we are not the owner of the video + if(!APP.conference.isLocalId(this.from)) + return; + + let state = this.player.getPlayerState(); + // if its paused and haven't been pause - send paused + if (state === YT.PlayerState.PAUSED && !this.playerPaused) { + this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, + this.url, 'pause'); + } + // if its playing and it was paused - send update with time + // if its playing and was playing just send update with time + else if (state === YT.PlayerState.PLAYING) { + this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, + this.url, 'playing', + this.player.getCurrentTime(), + this.player.isMuted() ? 0 : this.player.getVolume()); + } + } + + /** + * Updates video, if its not playing and needs starting or + * if its playing and needs to be paysed + * @param url the video url + * @param attributes + */ + updateSharedVideo (url, attributes) { + // if we are sending the event ignore + if(APP.conference.isLocalId(this.from)) { + return; + } + + if (attributes.state == 'playing') { + + if(!this.isSharedVideoShown) { + this.showSharedVideo(url, attributes); + return; + } + + // ocasionally we get this.player.getCurrentTime is not a function + // it seems its that player hasn't really loaded + if(!this.player || !this.player.getCurrentTime) + return; + + // check received time and current time + let currentPosition = this.player.getCurrentTime(); + let diff = Math.abs(attributes.time - currentPosition); + + // if we drift more than two times of the interval for checking + // sync, the interval is in milliseconds + if(diff > this.updateInterval*2/1000) { + console.log("Player seekTo:", attributes.time, + " current time is:", currentPosition, " diff:", diff); + this.player.seekTo(attributes.time); + } + + // lets check the volume + if (attributes.volume !== undefined && + this.player.getVolume() != attributes.volume) { + this.player.setVolume(attributes.volume); + console.log("Player change of volume:" + attributes.volume); + } + + + if(this.playerPaused) + this.player.playVideo(); + } else if (attributes.state == 'pause') { + // if its not paused, pause it + if(this.isSharedVideoShown) { + this.player.pauseVideo(); + } + else { + // if not shown show it, passing attributes so it can + // be shown paused + this.showSharedVideo(url, attributes); + } + } + } + + /** + * Stop shared video if it is currently showed. If the user started the + * shared video is the one in the attributes.from (called when user + * left and we want to remove video if the user sharing it left). + * @param attributes + */ + stopSharedVideo (attributes) { + if (!this.isSharedVideoShown) + return; + + if(this.from !== attributes.from) + return; + + if(this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + VideoLayout.removeParticipantContainer(this.url); + + VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false) + .then(() => { + VideoLayout.removeLargeVideoContainer( + SHARED_VIDEO_CONTAINER_TYPE); + + this.player.destroy(); + this.player = null; + }); + + this.url = null; + this.isSharedVideoShown = false; + } +} + +/** + * Container for shared video iframe. + */ +class SharedVideoContainer extends LargeContainer { + + constructor ({url, iframe, player}) { + super(); + + this.$iframe = $(iframe); + this.url = url; + this.player = player; + } + + get $video () { + return this.$iframe; + } + + show () { + return new Promise(resolve => { + this.$iframe.fadeIn(300, () => { + this.$iframe.css({opacity: 1}); + resolve(); + }); + }); + } + + hide () { + return new Promise(resolve => { + this.$iframe.fadeOut(300, () => { + this.$iframe.css({opacity: 0}); + resolve(); + }); + }); + } + + onHoverIn () { + ToolbarToggler.showToolbar(); + } + + get id () { + return this.url; + } + + resize (containerWidth, containerHeight) { + let height = containerHeight - FilmStrip.getFilmStripHeight(); + + let width = containerWidth; + + this.$iframe.width(width).height(height); + } + + /** + * @return {boolean} do not switch on dominant speaker event if on stage. + */ + stayOnStage () { + return false; + } +} + +function SharedVideoThumb (url) +{ + this.id = url; + + this.url = url; + this.setVideoType(SHARED_VIDEO_CONTAINER_TYPE); + this.videoSpanId = "sharedVideoContainer"; + this.container = this.createContainer(this.videoSpanId); + this.container.onclick = this.videoClick.bind(this); + //this.bindHoverHandler(); + + SmallVideo.call(this, VideoLayout); + this.isVideoMuted = true; +} +SharedVideoThumb.prototype = Object.create(SmallVideo.prototype); +SharedVideoThumb.prototype.constructor = SharedVideoThumb; + +/** + * hide display name + */ + +SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function () {}; + +SharedVideoThumb.prototype.avatarChanged = function () {}; + +SharedVideoThumb.prototype.createContainer = function (spanId) { + var container = document.createElement('span'); + container.id = spanId; + container.className = 'videocontainer'; + + // add the avatar + var avatar = document.createElement('img'); + avatar.id = 'avatar_' + this.id; + avatar.className = 'sharedVideoAvatar'; + avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg"; + container.appendChild(avatar); + + var remotes = document.getElementById('remoteVideos'); + return remotes.appendChild(container); +}; + +/** + * The thumb click handler. + */ +SharedVideoThumb.prototype.videoClick = function () { + VideoLayout.handleVideoThumbClicked(true, this.url); + VideoLayout.showLargeVideoContainer(this.videoType, true); +}; + +/** + * Removes RemoteVideo from the page. + */ +SharedVideoThumb.prototype.remove = function () { + console.log("Remove shared video thumb", this.id); + + // Remove whole container + if (this.container.parentNode) { + this.container.parentNode.removeChild(this.container); + } +}; + +/** + * Sets the display name for the thumb. + */ +SharedVideoThumb.prototype.setDisplayName = function(displayName) { + if (!this.container) { + console.warn( "Unable to set displayName - " + this.videoSpanId + + " does not exist"); + return; + } + + var nameSpan = $('#' + this.videoSpanId + '>span.displayname'); + + // If we already have a display name for this video. + if (nameSpan.length > 0) { + if (displayName && displayName.length > 0) { + $('#' + this.videoSpanId + '_name').text(displayName); + } + } else { + nameSpan = document.createElement('span'); + nameSpan.className = 'displayname'; + $('#' + this.videoSpanId)[0].appendChild(nameSpan); + + if (displayName && displayName.length > 0) + $(nameSpan).text(displayName); + nameSpan.id = this.videoSpanId + '_name'; + } + +}; + +/** + * Checks if given string is youtube url. + * @param {string} url string to check. + * @returns {boolean} + */ +function getYoutubeLink(url) { + let p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;//jshint ignore:line + return (url.match(p)) ? RegExp.$1 : false; +} + +/** + * Ask user for shared video url to share with others. + * Dialog validates client input to allow only youtube urls. + */ +function requestVideoLink() { + let i18n = APP.translation; + const title = i18n.generateTranslationHTML("dialog.shareVideoTitle"); + const cancelButton = i18n.generateTranslationHTML("dialog.Cancel"); + const shareButton = i18n.generateTranslationHTML("dialog.Share"); + const backButton = i18n.generateTranslationHTML("dialog.Back"); + const linkError + = i18n.generateTranslationHTML("dialog.shareVideoLinkError"); + const i18nOptions = {url: defaultSharedVideoLink}; + const defaultUrl = i18n.translateString("defaultLink", i18nOptions); + + return new Promise(function (resolve, reject) { + let dialog = messageHandler.openDialogWithStates({ + state0: { + html: ` +

${title}

+ `, + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: shareButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + if (!v) { + reject('cancelled'); + dialog.close(); + return; + } + + let sharedVideoUrl = f.sharedVideoUrl; + if (!sharedVideoUrl) { + return; + } + + let urlValue = encodeURI(UIUtil.escapeHtml(sharedVideoUrl)); + let yVideoId = getYoutubeLink(urlValue); + if (!yVideoId) { + dialog.goToState('state1'); + return false; + } + + resolve(yVideoId); + dialog.close(); + } + }, + + state1: { + html: `

${title}

${linkError}`, + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: backButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + if (v === 0) { + reject(); + dialog.close(); + } else { + dialog.goToState('state0'); + } + } + } + }); + + }); +} + diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 992af2d22..890b13bb2 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -126,6 +126,10 @@ const buttonHandlers = { AnalyticsAdapter.sendEvent('toolbar.etherpad.clicked'); emitter.emit(UIEvents.ETHERPAD_CLICKED); }, + "toolbar_button_sharedvideo": function () { + AnalyticsAdapter.sendEvent('toolbar.sharedvideo.clicked'); + emitter.emit(UIEvents.SHARED_VIDEO_CLICKED); + }, "toolbar_button_desktopsharing": function () { if (APP.conference.isSharingScreen) { AnalyticsAdapter.sendEvent('toolbar.screen.disabled'); @@ -284,6 +288,15 @@ const Toolbar = { } }, + // Shows or hides the 'shared video' button. + showSharedVideoButton (show) { + if (UIUtil.isButtonEnabled('sharedvideo') && show) { + $('#toolbar_button_sharedvideo').css({display: "inline-block"}); + } else { + $('#toolbar_button_sharedvideo').css({display: "none"}); + } + }, + // checks whether recording is enabled and whether we have params // to start automatically recording checkAutoRecord () { diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 494062af9..61a2d46c4 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -11,6 +11,8 @@ import {createDeferred} from '../../util/helpers'; const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE; const FADE_DURATION_MS = 300; +export const VIDEO_CONTAINER_TYPE = "camera"; + /** * Get stream id. * @param {JitsiTrack?} stream @@ -150,8 +152,6 @@ function getDesktopVideoPosition(videoWidth, return { horizontalIndent, verticalIndent }; } -export const VideoContainerType = "camera"; - /** * Container for user video. */ @@ -365,9 +365,10 @@ export default class LargeVideoManager { constructor () { this.containers = {}; - this.state = VideoContainerType; - this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType)); - this.addContainer(VideoContainerType, this.videoContainer); + this.state = VIDEO_CONTAINER_TYPE; + this.videoContainer = new VideoContainer( + () => this.resizeContainer(VIDEO_CONTAINER_TYPE)); + this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer); // use the same video container to handle and desktop tracks this.addContainer("desktop", this.videoContainer); @@ -616,7 +617,7 @@ export default class LargeVideoManager { } let oldContainer = this.containers[this.state]; - if (this.state === VideoContainerType) { + if (this.state === VIDEO_CONTAINER_TYPE) { this.showWatermark(false); } oldContainer.hide(); @@ -625,7 +626,7 @@ export default class LargeVideoManager { let container = this.getContainer(type); return container.show().then(() => { - if (type === VideoContainerType) { + if (type === VIDEO_CONTAINER_TYPE) { this.showWatermark(true); } }); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index fa5bac0bf..055bc460d 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -363,14 +363,7 @@ SmallVideo.prototype.updateView = function () { } setVisibility(avatar, showAvatar); - var showDisplayName = !showVideo && !showAvatar; - - if (showDisplayName) { - this.showDisplayName(this.VideoLayout.isLargeVideoVisible()); - } - else { - this.showDisplayName(false); - } + this.showDisplayName(!showVideo && !showAvatar); }; SmallVideo.prototype.avatarChanged = function (avatarUrl) { diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 22e6e16ef..e5b4130b3 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -9,7 +9,8 @@ import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from "../util/UIUtil"; import RemoteVideo from "./RemoteVideo"; -import LargeVideoManager, {VideoContainerType} from "./LargeVideo"; +import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo"; +import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo'; import LocalVideo from "./LocalVideo"; import PanelToggler from "../side_pannels/SidePanelToggler"; @@ -343,7 +344,7 @@ var VideoLayout = { let videoType = VideoLayout.getRemoteVideoType(id); if (!videoType) { // make video type the default one (camera) - videoType = VideoContainerType; + videoType = VIDEO_CONTAINER_TYPE; } remoteVideo.setVideoType(videoType); @@ -367,7 +368,8 @@ var VideoLayout = { // current dominant, focused speaker or update it to // the current dominant speaker. if ((!focusedVideoResourceJid && - !currentDominantSpeaker) || + !currentDominantSpeaker && + !this.isLargeContainerTypeVisible(SHARED_VIDEO_CONTAINER_TYPE)) || focusedVideoResourceJid === resourceJid || (resourceJid && currentDominantSpeaker === resourceJid)) { @@ -888,7 +890,7 @@ var VideoLayout = { }, isLargeVideoVisible () { - return this.isLargeContainerTypeVisible(VideoContainerType); + return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE); }, /** @@ -960,8 +962,17 @@ var VideoLayout = { return Promise.resolve(); } + let currentId = largeVideo.id; + if(currentId) { + var oldSmallVideo = this.getSmallVideo(currentId); + } + // if !show then use default type - large video - return largeVideo.showContainer(show ? type : VideoContainerType); + return largeVideo.showContainer(show ? type : VIDEO_CONTAINER_TYPE) + .then(() => { + if(oldSmallVideo) + oldSmallVideo && oldSmallVideo.updateView(); + }); }, isLargeContainerTypeVisible (type) { @@ -970,10 +981,18 @@ var VideoLayout = { /** * Returns the id of the current video shown on large. - * Currently used by tests (troture). + * Currently used by tests (torture). */ getLargeVideoID () { return largeVideo.id; + }, + + /** + * Returns the the current video shown on large. + * Currently used by tests (torture). + */ + getLargeVideo () { + return largeVideo; } }; diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index cc507164c..5444ebb0a 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -21,6 +21,13 @@ export default { AUDIO_MUTED: "UI.audio_muted", VIDEO_MUTED: "UI.video_muted", ETHERPAD_CLICKED: "UI.etherpad_clicked", + SHARED_VIDEO_CLICKED: "UI.start-shared-video", + /** + * Updates shared video with params: url, state, time(optional) + * Where url is the video link, state is stop/start/pause and time is the + * current video playing time. + */ + UPDATE_SHARED_VIDEO: "UI.update-shared-video", ROOM_LOCK_CLICKED: "UI.room_lock_clicked", USER_INVITED: "UI.user_invited", USER_KICKED: "UI.user_kicked", From a1ac18a6328f0b22a762b94cefd1321229913aa8 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 18 Mar 2016 16:47:47 -0500 Subject: [PATCH 19/51] Fixes pausing. --- modules/UI/shared_video/SharedVideo.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 5b7d16620..9ad4b001f 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -101,7 +101,7 @@ export default class SharedVideoManager { self.updateCheck(); } else if (event.data == YT.PlayerState.PAUSED) { self.playerPaused = true; - self.updateCheck(); + self.updateCheck(true); } }; @@ -146,7 +146,7 @@ export default class SharedVideoManager { /** * Checks current state of the player and fire an event with the values. */ - updateCheck() + updateCheck(sendPauseEvent) { // ignore update checks if we are not the owner of the video if(!APP.conference.isLocalId(this.from)) @@ -154,7 +154,7 @@ export default class SharedVideoManager { let state = this.player.getPlayerState(); // if its paused and haven't been pause - send paused - if (state === YT.PlayerState.PAUSED && !this.playerPaused) { + if (state === YT.PlayerState.PAUSED && sendPauseEvent) { this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, this.url, 'pause'); } @@ -189,7 +189,12 @@ export default class SharedVideoManager { // ocasionally we get this.player.getCurrentTime is not a function // it seems its that player hasn't really loaded - if(!this.player || !this.player.getCurrentTime) + if(!this.player || !this.player.getCurrentTime + || !this.player.pauseVideo + || !this.player.playVideo + || !this.player.getVolume + || !this.player.seekTo + || !this.player.getVolume) return; // check received time and current time @@ -211,7 +216,6 @@ export default class SharedVideoManager { console.log("Player change of volume:" + attributes.volume); } - if(this.playerPaused) this.player.playVideo(); } else if (attributes.state == 'pause') { From ba01733c4f3e9b4182e400d7c3a5ba4242e05ea3 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 18 Mar 2016 17:04:13 -0500 Subject: [PATCH 20/51] Adds confirmation dialog when closing shared video. --- lang/main.json | 2 ++ modules/UI/shared_video/SharedVideo.js | 27 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lang/main.json b/lang/main.json index 12d579de8..5125714df 100644 --- a/lang/main.json +++ b/lang/main.json @@ -163,6 +163,8 @@ "Remove": "Remove", "shareVideoTitle": "Share a video", "shareVideoLinkError": "Please provide a correct youtube link.", + "removeSharedVideoTitle": "Remove shared video", + "removeSharedVideoMsg": "Are you sure you would like to remove your shared video?", "WaitingForHost": "Waiting for the host ...", "WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "IamHost": "I am the host", diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 9ad4b001f..3812b55af 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -43,7 +43,8 @@ export default class SharedVideoManager { return; } - this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop'); + proposeToClose().then(() => + this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop')); } /** @@ -424,6 +425,30 @@ function getYoutubeLink(url) { return (url.match(p)) ? RegExp.$1 : false; } +/** + * Ask user if he want to close shared video. + */ +function proposeToClose() { + return new Promise(function (resolve, reject) { + messageHandler.openTwoButtonDialog( + "dialog.removeSharedVideoTitle", + null, + "dialog.removeSharedVideoMsg", + null, + false, + "dialog.Remove", + function(e,v,m,f) { + if (v) { + resolve(); + } else { + reject(); + } + } + ); + + }); +} + /** * Ask user for shared video url to share with others. * Dialog validates client input to allow only youtube urls. From 9e7275acfb1b3bfeecc999bfb80c71627831a1f4 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 18 Mar 2016 17:21:41 -0500 Subject: [PATCH 21/51] Updates large video when user left and we are removing the shared video. --- modules/UI/shared_video/SharedVideo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 3812b55af..edeeb70cb 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -380,6 +380,10 @@ SharedVideoThumb.prototype.videoClick = function () { SharedVideoThumb.prototype.remove = function () { console.log("Remove shared video thumb", this.id); + // Make sure that the large video is updated if are removing its + // corresponding small video. + this.VideoLayout.updateRemovedVideo(this.id); + // Remove whole container if (this.container.parentNode) { this.container.parentNode.removeChild(this.container); From d82d4cbed9d79b7520f5d97add3bfaa9be0a9a66 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 18 Mar 2016 17:44:32 -0500 Subject: [PATCH 22/51] Creates first the container before setting avatar, cause the avatar set will check the large video container type and type may be missing. Fixes error 'container of type undefined doesn't exist': > at LargeVideoManager.getContainer > at LargeVideoManager.get > at Object.isCurrentlyOnLarge > at Object.changeUserAvatar > at Object.UI.setUserAvatar > at Object.UI.addUser --- modules/UI/UI.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index fcd39c4fc..347776d40 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -490,11 +490,11 @@ UI.addUser = function (id, displayName) { config.startAudioMuted > APP.conference.membersCount) UIUtil.playSoundNotification('userJoined'); - // Configure avatar - UI.setUserAvatar(id); - // Add Peer's container VideoLayout.addParticipantContainer(id); + + // Configure avatar + UI.setUserAvatar(id); }; /** From 2c1a9d20fde41fb97c9500b3eac5a7ef90dc96ba Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 13:27:11 -0500 Subject: [PATCH 23/51] Fixes setting local video, default videoType. Fixes showing local video when there is no video device shown. --- modules/UI/videolayout/VideoLayout.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index e5b4130b3..7f5d3c5da 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -93,6 +93,11 @@ var VideoLayout = { init (emitter) { eventEmitter = emitter; localVideoThumbnail = new LocalVideo(VideoLayout, emitter); + // sets default video type of local video + localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); + // if we do not resize the thumbs here, if there is no video device + // the local video thumb maybe one pixel + this.resizeThumbnails(false, true, false); emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked); this.lastNCount = config.channelLastN; From 1bdeda4ec3b1ed8962a2d09fa3f21a8ad044ebcf Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 14:49:37 -0500 Subject: [PATCH 24/51] Fixes updateLarge video to change to different container types which do not have stream. --- modules/UI/shared_video/SharedVideo.js | 7 ++++--- modules/UI/videolayout/LargeVideo.js | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index edeeb70cb..f715cf59e 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -124,13 +124,15 @@ export default class SharedVideoManager { self.isSharedVideoShown = true; + // If we are sending the command and we are starting the player + // we need to continuously send the player current time position if(APP.conference.isLocalId(self.from)) { self.intervalId = setInterval( self.updateCheck.bind(self), self.updateInterval); } - // set initial state + // set initial state of the player if there is enough information if(attributes.state === 'pause') player.pauseVideo(); else if(attributes.time > 0) { @@ -334,7 +336,7 @@ function SharedVideoThumb (url) this.videoSpanId = "sharedVideoContainer"; this.container = this.createContainer(this.videoSpanId); this.container.onclick = this.videoClick.bind(this); - //this.bindHoverHandler(); + this.bindHoverHandler(); SmallVideo.call(this, VideoLayout); this.isVideoMuted = true; @@ -371,7 +373,6 @@ SharedVideoThumb.prototype.createContainer = function (spanId) { */ SharedVideoThumb.prototype.videoClick = function () { VideoLayout.handleVideoThumbClicked(true, this.url); - VideoLayout.showLargeVideoContainer(this.videoType, true); }; /** diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 61a2d46c4..ed73f9c27 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -452,7 +452,12 @@ export default class LargeVideoManager { // change the avatar url on large this.updateAvatar(Avatar.getAvatarUrl(id)); - let isVideoMuted = stream ? stream.isMuted() : true; + // If we the continer is VIDEO_CONTAINER_TYPE, we need to check + // its stream whether exist and is muted to set isVideoMuted + // in rest of the cases it is false + let isVideoMuted = false; + if (videoType == VIDEO_CONTAINER_TYPE) + isVideoMuted = stream ? stream.isMuted() : true; // show the avatar on large if needed container.showAvatar(isVideoMuted); From fe7d05a9513027d6ea966478d2caf0d0892803e7 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 14:56:50 -0500 Subject: [PATCH 25/51] Reverse the check to use only VIDEO_CONTAINER_TYPE. --- modules/UI/videolayout/VideoLayout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 7f5d3c5da..5ac92593b 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -374,7 +374,7 @@ var VideoLayout = { // the current dominant speaker. if ((!focusedVideoResourceJid && !currentDominantSpeaker && - !this.isLargeContainerTypeVisible(SHARED_VIDEO_CONTAINER_TYPE)) || + this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) || focusedVideoResourceJid === resourceJid || (resourceJid && currentDominantSpeaker === resourceJid)) { From 9a39898eea987f0bcd8aa58ac1ea0e6bc5476afe Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 14:57:36 -0500 Subject: [PATCH 26/51] Renames event name. --- service/UI/UIEvents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5444ebb0a..49272315e 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -27,7 +27,7 @@ export default { * Where url is the video link, state is stop/start/pause and time is the * current video playing time. */ - UPDATE_SHARED_VIDEO: "UI.update-shared-video", + UPDATE_SHARED_VIDEO: "UI.update_shared_video", ROOM_LOCK_CLICKED: "UI.room_lock_clicked", USER_INVITED: "UI.user_invited", USER_KICKED: "UI.user_kicked", From f0fd7d74355160a55a98f76366cc53f6f6e85580 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 14:59:03 -0500 Subject: [PATCH 27/51] Renames method. --- modules/UI/shared_video/SharedVideo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index f715cf59e..e65f7d12a 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -43,7 +43,7 @@ export default class SharedVideoManager { return; } - proposeToClose().then(() => + showStopVideoPropmpt().then(() => this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop')); } @@ -433,7 +433,7 @@ function getYoutubeLink(url) { /** * Ask user if he want to close shared video. */ -function proposeToClose() { +function showStopVideoPropmpt() { return new Promise(function (resolve, reject) { messageHandler.openTwoButtonDialog( "dialog.removeSharedVideoTitle", From 586b8401aef94c26b2f9e178700656fec9682a3a Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 15:04:52 -0500 Subject: [PATCH 28/51] Renames event name. --- service/UI/UIEvents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 49272315e..bcb18403a 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -21,7 +21,7 @@ export default { AUDIO_MUTED: "UI.audio_muted", VIDEO_MUTED: "UI.video_muted", ETHERPAD_CLICKED: "UI.etherpad_clicked", - SHARED_VIDEO_CLICKED: "UI.start-shared-video", + SHARED_VIDEO_CLICKED: "UI.start_shared_video", /** * Updates shared video with params: url, state, time(optional) * Where url is the video link, state is stop/start/pause and time is the From 8df6a2974156a7a51eef5b7f206236f972c39d12 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 22 Mar 2016 17:15:35 -0500 Subject: [PATCH 29/51] Removes hoverIn/Out detection for shared video that shows/hides displayname, as we always display the name of the video when there is no thumb shown. And we do not want the name over the thumb. --- modules/UI/shared_video/SharedVideo.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index e65f7d12a..efde0ba28 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -336,7 +336,6 @@ function SharedVideoThumb (url) this.videoSpanId = "sharedVideoContainer"; this.container = this.createContainer(this.videoSpanId); this.container.onclick = this.videoClick.bind(this); - this.bindHoverHandler(); SmallVideo.call(this, VideoLayout); this.isVideoMuted = true; From cc761700fe5c68b1d7af7be350c2a67cc850a786 Mon Sep 17 00:00:00 2001 From: yanas Date: Wed, 23 Mar 2016 20:43:29 -0500 Subject: [PATCH 30/51] 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 @@
+
+ +