Merge pull request #1587 from virtuacoplenny/lenny/vertical-filmstrip

vertical filmstrip and 1-on-1 mode
This commit is contained in:
yanas 2017-05-23 16:23:11 -05:00 committed by GitHub
commit 1ff89c5a1c
25 changed files with 667 additions and 67 deletions

View File

@ -1286,6 +1286,8 @@ export default {
// check the roles for the new user and reflect them // check the roles for the new user and reflect them
APP.UI.updateUserRole(user); APP.UI.updateUserRole(user);
updateRemoteThumbnailsVisibility();
}); });
room.on(ConferenceEvents.USER_LEFT, (id, user) => { room.on(ConferenceEvents.USER_LEFT, (id, user) => {
APP.store.dispatch(participantLeft(id, user)); APP.store.dispatch(participantLeft(id, user));
@ -1293,6 +1295,8 @@ export default {
APP.API.notifyUserLeft(id); APP.API.notifyUserLeft(id);
APP.UI.removeUser(id, user.getDisplayName()); APP.UI.removeUser(id, user.getDisplayName());
APP.UI.onSharedVideoStop(id); APP.UI.onSharedVideoStop(id);
updateRemoteThumbnailsVisibility();
}); });
room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => { room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
@ -1475,6 +1479,8 @@ export default {
reportError(e); reportError(e);
} }
} }
updateRemoteThumbnailsVisibility();
}); });
} }
@ -1811,6 +1817,8 @@ export default {
} }
}); });
} }
updateRemoteThumbnailsVisibility();
}); });
room.addCommandListener( room.addCommandListener(
this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => { this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => {
@ -1826,6 +1834,21 @@ export default {
APP.UI.onSharedVideoUpdate(id, value, attributes); APP.UI.onSharedVideoUpdate(id, value, attributes);
} }
}); });
function updateRemoteThumbnailsVisibility() {
const localUserId = APP.conference.getMyUserId();
const remoteParticipantsCount = room.getParticipantCount() - 1;
// Get the remote thumbnail count for cases where there are
// non-participants displaying video, such as with video sharing.
const remoteVideosCount = APP.UI.getRemoteVideosCount();
const shouldShowRemoteThumbnails = APP.UI.isPinned(localUserId)
|| remoteVideosCount > 1
|| remoteParticipantsCount !== remoteVideosCount;
APP.UI.setRemoteThumbnailsVisibility(shouldShowRemoteThumbnails);
}
}, },
/** /**
* Adds any room listener. * Adds any room listener.

View File

@ -57,6 +57,9 @@ var config = { // eslint-disable-line no-unused-vars
webrtcIceTcpDisable: false, webrtcIceTcpDisable: false,
openSctp: true, // Toggle to enable/disable SCTP channels openSctp: true, // Toggle to enable/disable SCTP channels
// Disable hiding of remote thumbnails when in a 1-on-1 conference call.
disable1On1Mode: false,
disableStats: false, disableStats: false,
disableAudioLevels: false, disableAudioLevels: false,
channelLastN: -1, // The default value of the channel attribute last-n. channelLastN: -1, // The default value of the channel attribute last-n.

View File

@ -62,10 +62,18 @@
videos. */ videos. */
font-size: 0pt; font-size: 0pt;
#filmstripLocalVideo {
padding-left: 0;
}
&.hidden { &.hidden {
bottom: -196px; bottom: -196px;
} }
.remote-videos-container {
display: flex;
}
.videocontainer { .videocontainer {
display: none; display: none;
position: relative; position: relative;
@ -130,4 +138,13 @@
margin-bottom: auto; margin-bottom: auto;
padding-right: $defaultToolbarSize; padding-right: $defaultToolbarSize;
} }
.remote-videos-container {
transition: opacity 1s;
&.hide-videos {
opacity: 0;
pointer-events: none;
}
}
} }

View File

@ -44,4 +44,28 @@
border-width: 5px; border-width: 5px;
border-bottom-width: 0; border-bottom-width: 0;
} }
/**
* Override default "top" styles to support popovers appearing from the
* left of the popover trigger element.
*/
&.left {
margin-left: -$popoverMenuPadding;
margin-top: 0;
.arrow {
border-color: transparent transparent transparent $popoverBg;
border-width: 5px 0px 5px 5px;
margin-left: 0;
margin-top: -5px;
}
.jitsipopover {
&__menu-padding {
bottom: 0;
height: 100%;
width: $popoverMenuPadding;
}
}
}
} }

View File

@ -0,0 +1,118 @@
/**
* Override other styles to support vertical filmstrip mode.
*/
.vertical-filmstrip {
.filmstrip {
align-items: flex-end;
box-sizing: border-box;
display: flex;
flex-direction: column-reverse;
height: 100%;
/**
* Hide videos by making them slight to the right.
*/
.filmstrip__videos {
right: 0;
transition: right 2s;
&.hidden {
bottom: auto;
right: -196px;
}
}
#filmstripLocalVideo {
height: auto;
justify-content: flex-end;
}
/**
* Remove unnecssary padding that is normally used to prevent horizontal
* filmstrip from overlapping the left edge of the screen.
*/
#filmstripLocalVideo,
#filmstripRemoteVideos {
padding: 0;
}
#filmstripRemoteVideos {
display: flex;
flex: 1;
flex-direction: column;
height: auto;
overflow-x: hidden !important;
.remote-videos-container {
flex-direction: column;
}
}
/**
* Rotate the hide filmstrip icon so it points towards the right edge
* of the screen.
*/
&__toolbar {
transform: rotate(-90deg);
}
/**
* Move the remote video menu trigger to the bottom left of the
* video thumbnail.
*/
.remotevideomenu {
bottom: 0;
left: 0;
top: auto;
right: auto;
transform: rotate(-90deg);
}
#remoteVideos {
flex-direction: column-reverse;
height: 100%;
}
.videocontainer {
/**
* Move status icons to the bottom right of the thumbnail.
*/
&__toolbar {
text-align: right;
.toolbar-icon {
float: none;
}
}
}
}
/**
* For video labels that display on the top right to adjust its position as
* the filmstrip itself or filmstrip remote videos appear and disappear.
*/
.video-state-indicator {
transition: right 2s;
&.with-filmstrip {
&#recordingLabel {
right: 200px;
}
&#videoResolutionLabel {
right: 150px;
}
}
}
/**
* Move toastr closer to the bottom of the screen and move left to avoid
* overlapping of videos when they are configured at default height.
*/
#toast-container {
&.notification-bottom-right {
bottom: 25px;
right: 130 + 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder;
}
}
}

View File

@ -73,5 +73,6 @@
@import 'policy'; @import 'policy';
@import 'filmstrip'; @import 'filmstrip';
@import 'unsupported-browser/main'; @import 'unsupported-browser/main';
@import 'vertical_filmstrip_overrides';
/* Modules END */ /* Modules END */

View File

@ -55,6 +55,12 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
* Whether to only show the filmstrip (and hide the toolbar). * Whether to only show the filmstrip (and hide the toolbar).
*/ */
filmStripOnly: false, filmStripOnly: false,
/**
* Whether to show thumbnails in filmstrip as a column instead of as a row.
*/
VERTICAL_FILMSTRIP: false,
//A html text to be shown to guests on the close page, false disables it //A html text to be shown to guests on the close page, false disables it
CLOSE_PAGE_GUEST_HINT: false, CLOSE_PAGE_GUEST_HINT: false,
RANDOM_AVATAR_URL_PREFIX: false, RANDOM_AVATAR_URL_PREFIX: false,

View File

@ -309,6 +309,10 @@ UI.start = function () {
SideContainerToggler.init(eventEmitter); SideContainerToggler.init(eventEmitter);
Filmstrip.init(eventEmitter); Filmstrip.init(eventEmitter);
// By default start with remote videos hidden and rely on other logic to
// make them visible.
UI.setRemoteThumbnailsVisibility(false);
VideoLayout.init(eventEmitter); VideoLayout.init(eventEmitter);
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
VideoLayout.initLargeVideo(); VideoLayout.initLargeVideo();
@ -339,6 +343,10 @@ UI.start = function () {
JitsiPopover.enabled = false; JitsiPopover.enabled = false;
} }
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$("body").addClass("vertical-filmstrip");
}
document.title = interfaceConfig.APP_NAME; document.title = interfaceConfig.APP_NAME;
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
@ -1142,6 +1150,15 @@ UI.getLargeVideo = function () {
return VideoLayout.getLargeVideo(); return VideoLayout.getLargeVideo();
}; };
/**
* Returns whether or not the passed in user id is currently pinned to the large
* video.
*
* @param {string} userId - The id of the user to check is pinned or not.
* @returns {boolean} True if the user is currently pinned to the large video.
*/
UI.isPinned = userId => VideoLayout.getPinnedId() === userId;
/** /**
* Shows dialog with a link to FF extension. * Shows dialog with a link to FF extension.
*/ */
@ -1392,6 +1409,23 @@ UI.isRingOverlayVisible = () => RingOverlay.isVisible();
*/ */
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user); UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
/**
* Returns the number of known remote videos.
*
* @returns {number} The number of remote videos.
*/
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
/**
* Makes remote thumbnail videos visible or not visible.
*
* @param {boolean} shouldHide - True if remote thumbnails should be hidden,
* false f they should be visible.
* @returns {void}
*/
UI.setRemoteThumbnailsVisibility
= shouldHide => Filmstrip.setRemoteVideoVisibility(shouldHide);
const UIListeners = new Map([ const UIListeners = new Map([
[ [
UIEvents.ETHERPAD_CLICKED, UIEvents.ETHERPAD_CLICKED,

View File

@ -201,6 +201,15 @@ function _showStopRecordingPrompt(recordingType) {
* position * position
*/ */
function moveToCorner(selector, move) { function moveToCorner(selector, move) {
const {
remoteVideosCount,
remoteVideosVisible,
visible
} = APP.store.getState()['features/filmstrip'];
selector.toggleClass(
'with-filmstrip',
Boolean(remoteVideosCount && remoteVideosVisible && visible));
let moveToCornerClass = "moveToCorner"; let moveToCornerClass = "moveToCorner";
let containsClass = selector.hasClass(moveToCornerClass); let containsClass = selector.hasClass(moveToCornerClass);
@ -295,6 +304,10 @@ var Recording = {
APP.UI.messageHandler.enableNotifications(false); APP.UI.messageHandler.enableNotifications(false);
APP.UI.messageHandler.enablePopups(false); APP.UI.messageHandler.enablePopups(false);
} }
this.eventEmitter.addListener(UIEvents.UPDATED_FILMSTRIP_DISPLAY, () =>{
this._updateStatusLabel();
});
}, },
/** /**

View File

@ -456,6 +456,9 @@ export default class SharedVideoManager {
// revert to original behavior (prevents pausing // revert to original behavior (prevents pausing
// for participants not sharing the video to pause it) // for participants not sharing the video to pause it)
$("#sharedVideo").css("pointer-events","auto"); $("#sharedVideo").css("pointer-events","auto");
this.emitter.emit(
UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
}); });
this.url = null; this.url = null;
@ -656,7 +659,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg"; avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
container.appendChild(avatar); container.appendChild(avatar);
var remotes = document.getElementById('remoteVideos'); var remotes = document.getElementById('filmstripRemoteVideosContainer');
return remotes.appendChild(container); return remotes.appendChild(container);
}; };

View File

@ -1,4 +1,74 @@
/* global $ */ /* global $ */
const positionConfigurations = {
left: {
// Align the popover's right side to the target element.
my: 'right',
// Align the popover to the left side of the target element.
at: 'left',
// Force the popover to fit within the viewport.
collision: 'fit',
/**
* Callback invoked by jQuery UI tooltip.
*
* @param {Object} position - The top and bottom position the popover
* element should be set at.
* @param {Object} element. - Additional size and position information
* about the popover element and target.
* @param {Object} elements.element - Has position and size related data
* for the popover element itself.
* @param {Object} elements.target - Has position and size related data
* for the target element the popover displays from.
*/
using: function setPositionLeft(position, elements) {
const { element, target } = elements;
$('.jitsipopover').css({
display: 'table',
left: position.left,
top: position.top
});
// Move additional padding to the right edge of the popover and
// allow css to take care of width. The padding is used to maintain
// a hover state between the target and the popover.
$('.jitsipopover > .jitsipopover__menu-padding').css({
left: element.width
});
// Find the distance from the top of the popover to the center of
// the target and use that value to position the arrow to point to
// it.
const verticalCenterOfTarget = target.height / 2;
const verticalDistanceFromTops = target.top - element.top;
const verticalPositionOfTargetCenter
= verticalDistanceFromTops + verticalCenterOfTarget;
$('.jitsipopover > .arrow').css({
left: element.width,
top: verticalPositionOfTargetCenter
});
}
},
top: {
my: "bottom",
at: "top",
collision: "fit",
using: function setPositionTop(position, elements) {
var calcLeft = elements.target.left - elements.element.left +
elements.target.width/2;
$(".jitsipopover").css(
{top: position.top, left: position.left, display: "table"});
$(".jitsipopover > .arrow").css({left: calcLeft});
$(".jitsipopover > .jitsipopover__menu-padding").css(
{left: calcLeft - 50});
}
}
};
var JitsiPopover = (function () { var JitsiPopover = (function () {
/** /**
* The default options * The default options
@ -7,7 +77,8 @@ var JitsiPopover = (function () {
skin: 'white', skin: 'white',
content: '', content: '',
hasArrow: true, hasArrow: true,
onBeforePosition: undefined onBeforePosition: undefined,
position: 'top'
}; };
/** /**
@ -21,7 +92,6 @@ var JitsiPopover = (function () {
function JitsiPopover(element, options) function JitsiPopover(element, options)
{ {
this.options = Object.assign({}, defaultOptions, options); this.options = Object.assign({}, defaultOptions, options);
this.elementIsHovered = false; this.elementIsHovered = false;
this.popoverIsHovered = false; this.popoverIsHovered = false;
this.popoverShown = false; this.popoverShown = false;
@ -45,12 +115,15 @@ var JitsiPopover = (function () {
* Returns template for popover * Returns template for popover
*/ */
JitsiPopover.prototype.getTemplate = function () { JitsiPopover.prototype.getTemplate = function () {
const { hasArrow, position, skin } = this.options;
let arrow = ''; let arrow = '';
if (this.options.hasArrow) { if (hasArrow) {
arrow = '<div class="arrow"></div>'; arrow = '<div class="arrow"></div>';
} }
return ( return (
`<div class="jitsipopover ${this.options.skin}"> `<div class="jitsipopover ${skin} ${position}">
${arrow} ${arrow}
<div class="jitsipopover__content"></div> <div class="jitsipopover__content"></div>
<div class="jitsipopover__menu-padding"></div> <div class="jitsipopover__menu-padding"></div>
@ -129,21 +202,14 @@ var JitsiPopover = (function () {
* Refreshes the position of the popover. * Refreshes the position of the popover.
*/ */
JitsiPopover.prototype.refreshPosition = function () { JitsiPopover.prototype.refreshPosition = function () {
$(".jitsipopover").position({ const positionOptions = Object.assign(
my: "bottom", {},
at: "top", positionConfigurations[this.options.position],
collision: "fit", {
of: this.element, of: this.element
using: function (position, elements) {
var calcLeft = elements.target.left - elements.element.left +
elements.target.width/2;
$(".jitsipopover").css(
{top: position.top, left: position.left, display: "table"});
$(".jitsipopover > .arrow").css({left: calcLeft});
$(".jitsipopover > .jitsipopover__menu-padding").css(
{left: calcLeft - 50});
} }
}); );
$(".jitsipopover").position(positionOptions);
}; };
/** /**

View File

@ -1,4 +1,4 @@
/* global $, APP */ /* global $, APP, interfaceConfig */
/* jshint -W101 */ /* jshint -W101 */
import JitsiPopover from "../util/JitsiPopover"; import JitsiPopover from "../util/JitsiPopover";
@ -309,7 +309,8 @@ ConnectionIndicator.prototype.create = function () {
this.popover = new JitsiPopover($(element), { this.popover = new JitsiPopover($(element), {
content: popoverContent, content: popoverContent,
skin: "black", skin: "black",
onBeforePosition: el => APP.translation.translateElement(el) onBeforePosition: el => APP.translation.translateElement(el),
position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
}); });
// override popover show method to make sure we will update the content // override popover show method to make sure we will update the content

View File

@ -1,4 +1,9 @@
/* global $, APP, JitsiMeetJS, interfaceConfig */ /* global $, APP, config, JitsiMeetJS, interfaceConfig */
import {
setFilmstripRemoteVideosVisibility,
setFilmstripVisibility
} from '../../../react/features/filmstrip';
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
@ -14,6 +19,7 @@ const Filmstrip = {
this.iconMenuUpClassName = 'icon-menu-up'; this.iconMenuUpClassName = 'icon-menu-up';
this.filmstripContainerClassName = 'filmstrip'; this.filmstripContainerClassName = 'filmstrip';
this.filmstrip = $('#remoteVideos'); this.filmstrip = $('#remoteVideos');
this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer');
this.eventEmitter = eventEmitter; this.eventEmitter = eventEmitter;
// Show the toggle button and add event listeners only when out of // Show the toggle button and add event listeners only when out of
@ -24,6 +30,28 @@ const Filmstrip = {
} }
}, },
/**
* Sets a class on the remote videos container for CSS to adjust visibility
* of the remote videos. Will no-op if config.debug is truthy, as should be
* the case with torture tests.
*
* @param {boolean} shouldHide - True if remote videos should be hidden,
* false if they should be visible.
* @returns {void}
*/
setRemoteVideoVisibility(shouldShow) {
// FIXME Checking config.debug is a grand hack to avoid fixing the
// torture tests after the 1-on-1 UI was implemented, which hides remote
// videos on 1-on-1 calls. If this check is to be kept, at least create
// new torture tests to verify 1-on-1 mode.
if (config.debug || config.disable1On1Mode) {
return;
}
APP.store.dispatch(setFilmstripRemoteVideosVisibility(shouldShow));
this.filmstripRemoteVideos.toggleClass('hide-videos', !shouldShow);
},
/** /**
* Initializes the filmstrip toolbar. * Initializes the filmstrip toolbar.
*/ */
@ -150,11 +178,14 @@ const Filmstrip = {
// Emit/fire UIEvents.TOGGLED_FILMSTRIP. // Emit/fire UIEvents.TOGGLED_FILMSTRIP.
const eventEmitter = this.eventEmitter; const eventEmitter = this.eventEmitter;
const isFilmstripVisible = this.isFilmstripVisible();
if (eventEmitter) { if (eventEmitter) {
eventEmitter.emit( eventEmitter.emit(
UIEvents.TOGGLED_FILMSTRIP, UIEvents.TOGGLED_FILMSTRIP,
this.isFilmstripVisible()); this.isFilmstripVisible());
} }
APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible));
}, },
/** /**
@ -177,7 +208,10 @@ const Filmstrip = {
* @returns {number} height * @returns {number} height
*/ */
getFilmstripHeight() { getFilmstripHeight() {
if (this.isFilmstripVisible()) { // FIXME Make it more clear the getFilmstripHeight check is used in
// horizontal film strip mode for calculating how tall large video
// display should be.
if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) {
return $(`.${this.filmstripContainerClassName}`).outerHeight(); return $(`.${this.filmstripContainerClassName}`).outerHeight();
} else { } else {
return 0; return 0;
@ -364,13 +398,27 @@ const Filmstrip = {
(remoteLocalWidthRatio * numberRemoteThumbs + 1), availableHeight * (remoteLocalWidthRatio * numberRemoteThumbs + 1), availableHeight *
interfaceConfig.LOCAL_THUMBNAIL_RATIO); interfaceConfig.LOCAL_THUMBNAIL_RATIO);
const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO; const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
return {
localVideo:{ const removeVideoWidth = lW * remoteLocalWidthRatio;
let localVideo;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
// scale both width and height
localVideo = {
thumbWidth: removeVideoWidth,
thumbHeight: h * remoteLocalWidthRatio
};
} else {
localVideo = {
thumbWidth: lW, thumbWidth: lW,
thumbHeight: h thumbHeight: h
}, };
}
return {
localVideo,
remoteVideo: { remoteVideo: {
thumbWidth: lW * remoteLocalWidthRatio, thumbWidth: removeVideoWidth,
thumbHeight: h thumbHeight: h
} }
}; };
@ -406,10 +454,15 @@ const Filmstrip = {
})); }));
} }
promises.push(new Promise((resolve) => { promises.push(new Promise((resolve) => {
// Let CSS take care of height in vertical filmstrip mode.
if (interfaceConfig.VERTICAL_FILMSTRIP) {
resolve();
} else {
this.filmstrip.animate({ this.filmstrip.animate({
// adds 2 px because of small video 1px border // adds 2 px because of small video 1px border
height: remote.thumbHeight + 2 height: remote.thumbHeight + 2
}, this._getAnimateOptions(animate, resolve)); }, this._getAnimateOptions(animate, resolve));
}
})); }));
promises.push(new Promise(() => { promises.push(new Promise(() => {
@ -456,8 +509,7 @@ const Filmstrip = {
} }
let localThumb = $("#localVideoContainer"); let localThumb = $("#localVideoContainer");
let remoteThumbs = this.filmstrip.children(selector) let remoteThumbs = this.filmstripRemoteVideos.children(selector);
.not("#localVideoContainer");
// Exclude the local video container if it has been hidden. // Exclude the local video container if it has been hidden.
if (localThumb.hasClass("hidden")) { if (localThumb.hasClass("hidden")) {

View File

@ -91,7 +91,8 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
content: popupMenuElement.outerHTML, content: popupMenuElement.outerHTML,
skin: "black", skin: "black",
hasArrow: false, hasArrow: false,
onBeforePosition: el => APP.translation.translateElement(el) onBeforePosition: el => APP.translation.translateElement(el),
position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
}; };
let element = $("#" + this.videoSpanId + " .remotevideomenu"); let element = $("#" + this.videoSpanId + " .remotevideomenu");
this.popover = new JitsiPopover(element, options); this.popover = new JitsiPopover(element, options);
@ -800,7 +801,7 @@ RemoteVideo.createContainer = function (spanId) {
overlay.className = "videocontainer__hoverOverlay"; overlay.className = "videocontainer__hoverOverlay";
container.appendChild(overlay); container.appendChild(overlay);
var remotes = document.getElementById('remoteVideos'); var remotes = document.getElementById('filmstripRemoteVideosContainer');
return remotes.appendChild(container); return remotes.appendChild(container);
}; };

View File

@ -1,6 +1,10 @@
/* global APP, $, interfaceConfig */ /* global APP, $, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename); const logger = require("jitsi-meet-logger").getLogger(__filename);
import {
setFilmstripRemoteVideosCount
} from '../../../react/features/filmstrip';
import Filmstrip from "./Filmstrip"; import Filmstrip from "./Filmstrip";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
@ -550,6 +554,9 @@ var VideoLayout = {
if (onComplete && typeof onComplete === "function") if (onComplete && typeof onComplete === "function")
onComplete(); onComplete();
}); });
APP.store.dispatch(
setFilmstripRemoteVideosCount(this.getRemoteVideosCount()));
return { localVideo, remoteVideo }; return { localVideo, remoteVideo };
}, },
@ -1133,6 +1140,15 @@ var VideoLayout = {
*/ */
getLargeVideoWrapper() { getLargeVideoWrapper() {
return this.getCurrentlyOnLargeContainer().$wrapper; return this.getCurrentlyOnLargeContainer().$wrapper;
},
/**
* Returns the number of remove video ids.
*
* @returns {number} The number of remote videos.
*/
getRemoteVideosCount() {
return Object.keys(remoteVideos).length;
} }
}; };

View File

@ -10,6 +10,7 @@ import { OverlayContainer } from '../../overlay';
import { Toolbox } from '../../toolbox'; import { Toolbox } from '../../toolbox';
import { HideNotificationBarStyle } from '../../unsupported-browser'; import { HideNotificationBarStyle } from '../../unsupported-browser';
import { VideoStatusLabel } from '../../video-status-label'; import { VideoStatusLabel } from '../../video-status-label';
import '../../filmstrip';
declare var $: Function; declare var $: Function;
declare var APP: Object; declare var APP: Object;
@ -106,10 +107,31 @@ class Conference extends Component {
src = 'images/spin.svg' /> src = 'images/spin.svg' />
</span> </span>
</div> </div>
{ this._renderFilmstrip() }
</div>
<DialogContainer />
<OverlayContainer />
<HideNotificationBarStyle />
</div>
);
}
/**
* Creates a React Element for displaying filmstrip videos.
*
* @private
* @returns {ReactElement}
*/
_renderFilmstrip() {
return (
<div className = 'filmstrip'> <div className = 'filmstrip'>
<div <div
className = 'filmstrip__videos' className = 'filmstrip__videos'
id = 'remoteVideos'> id = 'remoteVideos'>
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
<span <span
className = 'videocontainer' className = 'videocontainer'
id = 'localVideoContainer'> id = 'localVideoContainer'>
@ -125,6 +147,23 @@ class Conference extends Component {
className className
= 'videocontainer__hoverOverlay' /> = 'videocontainer__hoverOverlay' />
</span> </span>
</div>
<div
className = 'filmstrip__videos'
id = 'filmstripRemoteVideos'>
{
/*
This extra video container is needed for
scrolling thumbnails in firefox, otherwise the
flex thumbnails resize instead of causing
overflow.
*/
}
<div
className = 'remote-videos-container'
id = 'filmstripRemoteVideosContainer' />
</div>
<audio <audio
id = 'userJoined' id = 'userJoined'
preload = 'auto' preload = 'auto'
@ -135,12 +174,6 @@ class Conference extends Component {
src = 'sounds/left.wav' /> src = 'sounds/left.wav' />
</div> </div>
</div> </div>
</div>
<DialogContainer />
<OverlayContainer />
<HideNotificationBarStyle />
</div>
); );
} }
} }

View File

@ -0,0 +1,35 @@
import { Symbol } from '../base/react';
/**
* The type of action which signals to change the count of known remote videos
* displayed in the filmstrip.
*
* {
* type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
* remoteVideosCount: number
* }
*/
export const SET_FILMSTRIP_REMOTE_VIDEOS_COUNT
= Symbol('SET_FILMSTRIP_REMOTE_VIDEOS_COUNT');
/**
* The type of action which signals to change the visibility of remote videos in
* the filmstrip.
*
* {
* type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
* removeVideosVisible: boolean
* }
*/
export const SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY
= Symbol('SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY');
/**
* The type of action sets the visibility of the entire filmstrip;
*
* {
* type: SET_FILMSTRIP_VISIBILITY,
* visible: boolean
* }
*/
export const SET_FILMSTRIP_VISIBILITY = Symbol('SET_FILMSTRIP_VISIBILITY');

View File

@ -0,0 +1,54 @@
import {
SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
SET_FILMSTRIP_VISIBILITY
} from './actionTypes';
/**
* Sets the visibility of remote videos in the filmstrip.
*
* @param {boolean} remoteVideosVisible - Whether or not remote videos in the
* filmstrip should be visible.
* @returns {{
* type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
* remoteVideosVisible: boolean
* }}
*/
export function setFilmstripRemoteVideosVisibility(remoteVideosVisible) {
return {
type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
remoteVideosVisible
};
}
/**
* Sets how many remote videos are currently in the filmstrip.
*
* @param {number} remoteVideosCount - The number of remote videos.
* @returns {{
* type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
* remoteVideosCount: number
* }}
*/
export function setFilmstripRemoteVideosCount(remoteVideosCount) {
return {
type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
remoteVideosCount
};
}
/**
* Sets if the entire filmstrip should be visible.
*
* @param {boolean} visible - Whether not the filmstrip is visible.
* @returns {{
* type: SET_FILMSTRIP_VISIBILITY,
* visible: boolean
* }}
*/
export function setFilmstripVisibility(visible) {
return {
type: SET_FILMSTRIP_VISIBILITY,
visible
};
}

View File

@ -1 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './components'; export * from './components';
import './middleware';
import './reducer';

View File

@ -0,0 +1,30 @@
import UIEvents from '../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../base/redux';
import {
SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
SET_FILMSTRIP_VISIBILITY
} from './actionTypes';
declare var APP: Object;
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case SET_FILMSTRIP_REMOTE_VIDEOS_COUNT:
case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
case SET_FILMSTRIP_VISIBILITY: {
if (typeof APP !== 'undefined') {
APP.UI.emitEvent(UIEvents.UPDATED_FILMSTRIP_DISPLAY);
}
break;
}
}
return result;
});

View File

@ -0,0 +1,36 @@
import { ReducerRegistry } from '../base/redux';
import {
SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
SET_FILMSTRIP_VISIBILITY
} from './actionTypes';
const DEFAULT_STATE = {
remoteVideosCount: 0,
remoteVideosVisible: true,
visible: true
};
ReducerRegistry.register(
'features/filmstrip',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_FILMSTRIP_REMOTE_VIDEOS_COUNT:
return {
...state,
remoteVideosCount: action.remoteVideosCount
};
case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
return {
...state,
remoteVideosVisible: action.remoteVideosVisible
};
case SET_FILMSTRIP_VISIBILITY:
return {
...state,
visible: action.visible
};
}
return state;
});

View File

@ -28,6 +28,11 @@ export class VideoStatusLabel extends Component {
*/ */
_conferenceStarted: React.PropTypes.bool, _conferenceStarted: React.PropTypes.bool,
/**
* Whether or not the filmstrip is displayed with remote videos.
*/
_filmstripVisible: React.PropTypes.bool,
/** /**
* Whether or not a high-definition large video is displayed. * Whether or not a high-definition large video is displayed.
*/ */
@ -64,7 +69,13 @@ export class VideoStatusLabel extends Component {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
const { _audioOnly, _conferenceStarted, _largeVideoHD, t } = this.props; const {
_audioOnly,
_conferenceStarted,
_filmstripVisible,
_largeVideoHD,
t
} = this.props;
// FIXME The _conferenceStarted check is used to be defensive against // FIXME The _conferenceStarted check is used to be defensive against
// toggling audio only mode while there is no conference and hides the // toggling audio only mode while there is no conference and hides the
@ -82,9 +93,14 @@ export class VideoStatusLabel extends Component {
? t('videoStatus.hd') : t('videoStatus.sd'); ? t('videoStatus.hd') : t('videoStatus.sd');
} }
const filmstripClassName
= _filmstripVisible ? 'with-filmstrip' : 'without-filmstrip';
const classNames
= `video-state-indicator moveToCorner ${filmstripClassName}`;
return ( return (
<div <div
className = 'video-state-indicator moveToCorner' className = { classNames }
id = 'videoResolutionLabel' > id = 'videoResolutionLabel' >
{ displayedLabel } { displayedLabel }
{ this._renderVideonMenu() } { this._renderVideonMenu() }
@ -152,10 +168,17 @@ function _mapStateToProps(state) {
conference, conference,
isLargeVideoHD isLargeVideoHD
} = state['features/base/conference']; } = state['features/base/conference'];
const {
remoteVideosCount,
remoteVideosVisible,
visible
} = state['features/filmstrip'];
return { return {
_audioOnly: audioOnly, _audioOnly: audioOnly,
_conferenceStarted: Boolean(conference), _conferenceStarted: Boolean(conference),
_filmstripVisible:
Boolean(remoteVideosCount && remoteVideosVisible && visible),
_largeVideoHD: isLargeVideoHD _largeVideoHD: isLargeVideoHD
}; };
} }

View File

@ -65,6 +65,12 @@ export default {
* @see {TOGGLE_FILMSTRIP} * @see {TOGGLE_FILMSTRIP}
*/ */
TOGGLED_FILMSTRIP: "UI.toggled_filmstrip", TOGGLED_FILMSTRIP: "UI.toggled_filmstrip",
/**
* Notifies that the filmstrip has updated its appearance, such as by
* toggling or removing videos or adding videos.
*/
UPDATED_FILMSTRIP_DISPLAY: "UI.updated_filmstrip_display",
TOGGLE_SCREENSHARING: "UI.toggle_screensharing", TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document", TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
CONTACT_CLICKED: "UI.contact_clicked", CONTACT_CLICKED: "UI.contact_clicked",