diff --git a/css/_utils.scss b/css/_utils.scss index 629000c56..7027559c2 100644 --- a/css/_utils.scss +++ b/css/_utils.scss @@ -31,13 +31,6 @@ display: inline-block !important; } -/** -* Shows as a list item -**/ -.show-list-item { - display: list-item !important; -} - /** * Shows a flex element. */ diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index c9676fd89..725c2ab96 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -173,7 +173,9 @@ &__hoverOverlay { background: rgba(0,0,0,.6); border-radius: $borderRadius; - position: relative; + position: absolute; + top: 0px; + left: 0px; width: 100%; height: 100%; visibility: hidden; @@ -526,13 +528,16 @@ display: flex; justify-content: center; height: 50%; - overflow: hidden; width: auto; + overflow: hidden; .userAvatar { height: 100%; object-fit: cover; width: 100%; + top: 0px; + left: 0px; + position: absolute; } } @@ -555,6 +560,9 @@ } .sharedVideoAvatar { + position: absolute; + left: 0px; + top: 0px; height: 100%; width: 100%; object-fit: cover; diff --git a/css/filmstrip/_horizontal_filmstrip.scss b/css/filmstrip/_horizontal_filmstrip.scss index 6db5a5892..640386e20 100644 --- a/css/filmstrip/_horizontal_filmstrip.scss +++ b/css/filmstrip/_horizontal_filmstrip.scss @@ -55,6 +55,7 @@ &#filmstripLocalVideo { align-self: flex-end; display: block; + margin-bottom: 8px; } &.hidden { @@ -108,6 +109,7 @@ */ padding: 1px 0; overflow-y: hidden; + overflow-x: scroll; } } diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss index 86b822358..166636a51 100644 --- a/css/filmstrip/_small_video.scss +++ b/css/filmstrip/_small_video.scss @@ -1,5 +1,5 @@ .filmstrip__videos .videocontainer { - display: none; + display: inline-block; position: relative; background-size: contain; border: $thumbnailVideoBorder solid transparent; diff --git a/css/filmstrip/_vertical_filmstrip.scss b/css/filmstrip/_vertical_filmstrip.scss index 1ad922862..b551dcf6a 100644 --- a/css/filmstrip/_vertical_filmstrip.scss +++ b/css/filmstrip/_vertical_filmstrip.scss @@ -25,6 +25,7 @@ display: flex; flex-direction: column-reverse; height: 100%; + width: 100%; padding: ($desktopAppDragBarHeight - 5px) 5px 10px; /** * fixed positioning is necessary for remote menus and tooltips to pop @@ -48,7 +49,6 @@ .filmstrip__videos { @extend %align-right; bottom: 0; - overflow: visible !important; padding: 0; position:relative; right: 0; @@ -67,6 +67,7 @@ border: $thumbnailsBorder solid transparent; padding-left: 0; transition: right 2s; + width: 100%; } } @@ -80,6 +81,16 @@ flex-direction: column-reverse; height: auto; justify-content: flex-start; + + #filmstripLocalVideoThumbnail { + width: calc(100% - 15px); + + .videocontainer { + height: 0px; + width: 100%; + } + } + } /** @@ -96,18 +107,21 @@ display: flex; flex: 1; - flex-direction: column; + flex-direction: column-reverse; height: auto; - justify-content: flex-end; + overflow-x: hidden; + overflow-y: scroll; #filmstripRemoteVideosContainer { + @include minHWAutoFix(); flex-direction: column-reverse; - /** - * Add padding as a hack for Firefox not to show scrollbars when - * unnecessary. - */ - padding: 1px 0; - overflow-x: hidden; + overflow: visible; + width: calc(100% - 8px); // 8px for margin + border of the thumbnails + + .videocontainer { + height: 0px; + width: 100%; + } } } @@ -160,9 +174,24 @@ } } +/** + * FF does not include the scroll width when calculating the size of the content. That's why we need to include + * ourselves the width of the scroll so that the remote videos are aligned with the local one. + */ +@mixin filmstripSizeWithoutScroll { + .vertical-filmstrip { + #remoteVideos #filmstripRemoteVideos { + #filmstripRemoteVideosContainer { + width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll + } + } + } +} + /** Firefox detection hack **/ @-moz-document url-prefix() { @include undoColumnReverseVideos(); + @include filmstripSizeWithoutScroll(); } /** Edge detection hack **/ diff --git a/css/filmstrip/_vertical_filmstrip_overrides.scss b/css/filmstrip/_vertical_filmstrip_overrides.scss index 3de53f3fc..1a21c6304 100644 --- a/css/filmstrip/_vertical_filmstrip_overrides.scss +++ b/css/filmstrip/_vertical_filmstrip_overrides.scss @@ -56,6 +56,10 @@ transform: translate3d(0, 0, 0); } + .indicator-icon-container { + display: inline-block; + } + .indicator-container { float: none; } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index eb884ca98..7619f694a 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -11,7 +11,6 @@ import EtherpadManager from './etherpad/Etherpad'; import SharedVideoManager from './shared_video/SharedVideo'; import VideoLayout from './videolayout/VideoLayout'; -import Filmstrip from './videolayout/Filmstrip'; import { getLocalParticipant } from '../../react/features/base/participants'; import { toggleChat } from '../../react/features/chat'; @@ -158,8 +157,6 @@ UI.start = function() { // Set the defaults for prompt dialogs. $.prompt.setDefaults({ persistent: false }); - Filmstrip.init(eventEmitter); - VideoLayout.init(eventEmitter); if (!interfaceConfig.filmStripOnly) { VideoLayout.initLargeVideo(); @@ -202,7 +199,7 @@ UI.bindEvents = () => { * */ function onResize() { - VideoLayout.resizeVideoArea(); + VideoLayout.onResize(); } // Resize and reposition videos in full screen mode. @@ -353,12 +350,6 @@ UI.toggleFilmstrip = function() { APP.store.dispatch(setFilmstripVisible(!visible)); }; -/** - * Checks if the filmstrip is currently visible or not. - * @returns {true} if the filmstrip is currently visible, and false otherwise. - */ -UI.isFilmstripVisible = () => Filmstrip.isFilmstripVisible(); - /** * Toggles the visibility of the chat panel. */ diff --git a/modules/UI/shared_video/SharedVideoThumb.js b/modules/UI/shared_video/SharedVideoThumb.js index 8f8bedfb1..a7b8ab719 100644 --- a/modules/UI/shared_video/SharedVideoThumb.js +++ b/modules/UI/shared_video/SharedVideoThumb.js @@ -17,17 +17,16 @@ export default class SharedVideoThumb extends SmallVideo { constructor(participant, videoType, VideoLayout) { super(VideoLayout); this.id = participant.id; - + this.isLocal = false; this.url = participant.id; this.setVideoType(videoType); this.videoSpanId = 'sharedVideoContainer'; this.container = this.createContainer(this.videoSpanId); this.$container = $(this.container); - + this._setThumbnailSize(); this.bindHoverHandler(); this.isVideoMuted = true; this.updateDisplayName(); - this.container.onclick = this._onContainerClick; } diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index ce7413e92..7e7f3c44b 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -1,22 +1,4 @@ -/* global $, interfaceConfig */ - -/** - * Associates the default display type with corresponding CSS class - */ -const SHOW_CLASSES = { - 'block': 'show', - 'inline': 'show-inline', - 'list-item': 'show-list-item' -}; - -/** - * Contains sizes of thumbnails - * @type {{SMALL: number, MEDIUM: number}} - */ -const ThumbnailSizes = { - SMALL: 60, - MEDIUM: 80 -}; +/* global $ */ /** * Created by hristo on 12/22/14. @@ -30,32 +12,6 @@ const UIUtil = { return window.innerWidth; }, - /** - * Changes the style class of the element given by id. - */ - buttonClick(id, classname) { - // add the class to the clicked element - $(`#${id}`).toggleClass(classname); - }, - - /** - * Returns the text width for the given element. - * - * @param el the element - */ - getTextWidth(el) { - return el.clientWidth + 1; - }, - - /** - * Returns the text height for the given element. - * - * @param el the element - */ - getTextHeight(el) { - return el.clientHeight + 1; - }, - /** * Escapes the given text. */ @@ -64,27 +20,6 @@ const UIUtil = { .html(); }, - imageToGrayScale(canvas) { - const context = canvas.getContext('2d'); - const imgData = context.getImageData(0, 0, canvas.width, canvas.height); - const pixels = imgData.data; - - for (let i = 0, n = pixels.length; i < n; i += 4) { - const grayscale - = (pixels[i] * 0.3) - + (pixels[i + 1] * 0.59) - + (pixels[i + 2] * 0.11); - - pixels[i] = grayscale; // red - pixels[i + 1] = grayscale; // green - pixels[i + 2] = grayscale; // blue - // pixels[i+3] is alpha - } - - // redraw the image in black & white - context.putImageData(imgData, 0, 0); - }, - /** * Inserts given child element as the first one into the container. * @param container the container to which new child element will be added @@ -100,81 +35,6 @@ const UIUtil = { } }, - /** - * Indicates if Authentication Section should be shown - * - * @returns {boolean} - */ - isAuthenticationEnabled() { - return interfaceConfig.AUTHENTICATION_ENABLE; - }, - - /** - * Shows / hides the element given by id. - * - * @param {string|HTMLElement} idOrElement the identifier or the element - * to show/hide - * @param {boolean} show true to show or false to hide - */ - setVisible(id, visible) { - let element; - - if (id instanceof HTMLElement) { - element = id; - } else { - element = document.getElementById(id); - } - - if (!element) { - return; - } - - if (!visible) { - element.classList.add('hide'); - } else if (element.classList.contains('hide')) { - element.classList.remove('hide'); - } - - const type = this._getElementDefaultDisplay(element.tagName); - const className = SHOW_CLASSES[type]; - - if (visible) { - element.classList.add(className); - } else if (element.classList.contains(className)) { - element.classList.remove(className); - } - }, - - /** - * Returns default display style for the tag - * @param tag - * @returns {*} - * @private - */ - _getElementDefaultDisplay(tag) { - const tempElement = document.createElement(tag); - - document.body.appendChild(tempElement); - const style = window.getComputedStyle(tempElement).display; - - document.body.removeChild(tempElement); - - return style; - }, - - /** - * Shows / hides the element with the given jQuery selector. - * - * @param {jQuery} jquerySelector the jQuery selector of the element to - * show / shide - * @param {boolean} isVisible - */ - setVisibleBySelector(jquerySelector, isVisible) { - if (jquerySelector && jquerySelector.length > 0) { - jquerySelector.css('visibility', isVisible ? 'visible' : 'hidden'); - } - }, - /** * Redirects to a given URL. * @@ -200,17 +60,6 @@ const UIUtil = { || document.msFullscreenElement); }, - /** - * Create html attributes string out of object properties. - * @param {Object} attrs object with properties - * @returns {String} string of html element attributes - */ - attrsToString(attrs) { - return ( - Object.keys(attrs).map(key => ` ${key}="${attrs[key]}"`) -.join(' ')); - }, - /** * Checks if the given DOM element is currently visible. The offsetParent * will be null if the "display" property of the element or any of its @@ -220,100 +69,6 @@ const UIUtil = { */ isVisible(el) { return el.offsetParent !== null; - }, - - /** - * Shows / hides the element given by {selector} and sets a timeout if the - * {hideDelay} is set to a value > 0. - * @param selector the jquery selector of the element to show/hide. - * @param show a {boolean} that indicates if the element should be shown or - * hidden - * @param hideDelay the value in milliseconds to wait before hiding the - * element - */ - animateShowElement(selector, show, hideDelay) { - if (show) { - if (!selector.is(':visible')) { - selector.css('display', 'inline-block'); - } - - selector.fadeIn(300, - () => { - selector.css({ opacity: 1 }); - } - ); - - if (hideDelay && hideDelay > 0) { - setTimeout( - () => { - selector.fadeOut( - 300, - () => { - selector.css({ opacity: 0 }); - }); - }, - hideDelay); - } - } else { - selector.fadeOut(300, - () => { - selector.css({ opacity: 0 }); - } - ); - } - }, - - /** - * Parses the given cssValue as an Integer. If the value is not a number - * we return 0 instead of NaN. - * @param cssValue the string value we obtain when querying css properties - */ - parseCssInt(cssValue) { - return parseInt(cssValue, 10) || 0; - }, - - /** - * Adds href value to 'a' link jquery object. If link value is null, - * undefined or empty string, disables the link. - * @param {object} aLinkElement the jquery object - * @param {string} link the link value - */ - setLinkHref(aLinkElement, link) { - if (link) { - aLinkElement.attr('href', link); - } else { - aLinkElement.css({ - 'pointer-events': 'none', - 'cursor': 'default' - }); - } - }, - - /** - * Returns font size for indicators according to current - * height of thumbnail - * @param {Number} [thumbnailHeight] - current height of thumbnail - * @returns {Number} - font size for current height - */ - getIndicatorFontSize(thumbnailHeight) { - const height = typeof thumbnailHeight === 'undefined' - ? $('#localVideoContainer').height() : thumbnailHeight; - - const { SMALL, MEDIUM } = ThumbnailSizes; - const IndicatorFontSizes = interfaceConfig.INDICATOR_FONT_SIZES || { - SMALL: 5, - MEDIUM: 6, - NORMAL: 8 - }; - let fontSize = IndicatorFontSizes.NORMAL; - - if (height <= SMALL) { - fontSize = IndicatorFontSizes.SMALL; - } else if (height > SMALL && height <= MEDIUM) { - fontSize = IndicatorFontSizes.MEDIUM; - } - - return fontSize; } }; diff --git a/modules/UI/videolayout/Filmstrip.js b/modules/UI/videolayout/Filmstrip.js index d140db54b..27c23ec58 100644 --- a/modules/UI/videolayout/Filmstrip.js +++ b/modules/UI/videolayout/Filmstrip.js @@ -1,33 +1,8 @@ /* global $, APP, interfaceConfig */ -import { - LAYOUTS, - getCurrentLayout, - getMaxColumnCount, - getTileViewGridDimensions, - shouldDisplayTileView -} from '../../../react/features/video-layout'; - -import UIUtil from '../util/UIUtil'; +import { isFilmstripVisible } from '../../../react/features/filmstrip'; const Filmstrip = { - /** - * Caches jquery lookups of the filmstrip for future use. - */ - init() { - this.filmstripContainerClassName = 'filmstrip'; - this.filmstrip = $('#remoteVideos'); - this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer'); - }, - - /** - * Shows if filmstrip is visible - * @returns {boolean} - */ - isFilmstripVisible() { - return APP.store.getState()['features/filmstrip'].visible; - }, - /** * Returns the height of filmstrip * @returns {number} height @@ -36,8 +11,8 @@ const Filmstrip = { // 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(); + if (isFilmstripVisible(APP.store) && !interfaceConfig.VERTICAL_FILMSTRIP) { + return $('.filmstrip').outerHeight(); } return 0; @@ -48,311 +23,143 @@ const Filmstrip = { * @returns {number} width */ getFilmstripWidth() { - return this.isFilmstripVisible() - ? this.filmstrip.outerWidth() - - parseInt(this.filmstrip.css('paddingLeft'), 10) - - parseInt(this.filmstrip.css('paddingRight'), 10) + const filmstrip = $('#remoteVideos'); + + return isFilmstripVisible(APP.store) + ? filmstrip.outerWidth() + - parseInt(filmstrip.css('paddingLeft'), 10) + - parseInt(filmstrip.css('paddingRight'), 10) : 0; }, /** - * Calculates the size for thumbnails: local and remote one - * @returns {*|{localVideo, remoteVideo}} - */ - calculateThumbnailSize() { - if (shouldDisplayTileView(APP.store.getState())) { - return this._calculateThumbnailSizeForTileView(); - } - - const { availableWidth, availableHeight } = this.calculateAvailableSize(); - - return this.calculateThumbnailSizeFromAvailable(availableWidth, availableHeight); - }, - - /** - * Calculates available size for one thumbnail according to - * the current window size. + * Resizes thumbnails for tile view. * - * @returns {{availableWidth: number, availableHeight: number}} + * @param {number} width - The new width of the thumbnails. + * @param {number} height - The new height of the thumbnails. + * @param {boolean} forceUpdate + * @returns {void} */ - calculateAvailableSize() { - /** - * If the videoAreaAvailableWidth is set we use this one to calculate - * the filmstrip width, because we're probably in a state where the - * filmstrip size hasn't been updated yet, but it will be. - */ - const videoAreaAvailableWidth - = UIUtil.getAvailableVideoWidth() - - this._getFilmstripExtraPanelsWidth() - - UIUtil.parseCssInt(this.filmstrip.css('right'), 10) - - UIUtil.parseCssInt(this.filmstrip.css('paddingLeft'), 10) - - UIUtil.parseCssInt(this.filmstrip.css('paddingRight'), 10) - - UIUtil.parseCssInt(this.filmstrip.css('borderLeftWidth'), 10) - - UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10) - - 5; - - const availableHeight = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120, window.innerHeight - 18); - let availableWidth = videoAreaAvailableWidth; - const localVideoContainer = $('#localVideoContainer'); - - // If local thumb is not hidden - if (!localVideoContainer.has('hidden')) { - availableWidth = Math.floor( - videoAreaAvailableWidth - ( - UIUtil.parseCssInt(localVideoContainer.css('borderLeftWidth'), 10) - + UIUtil.parseCssInt(localVideoContainer.css('borderRightWidth'), 10) - + UIUtil.parseCssInt(localVideoContainer.css('paddingLeft'), 10) - + UIUtil.parseCssInt(localVideoContainer.css('paddingRight'), 10) - + UIUtil.parseCssInt(localVideoContainer.css('marginLeft'), 10) - + UIUtil.parseCssInt(localVideoContainer.css('marginRight'), 10)) - ); - } - - return { - availableHeight, - availableWidth - }; - }, - - /** - * Traverse all elements inside the filmstrip - * and calculates the sum of all of them except - * remote videos element. Used for calculation of - * available width for video thumbnails. - * - * @returns {number} calculated width - * @private - */ - _getFilmstripExtraPanelsWidth() { - const className = this.filmstripContainerClassName; - let width = 0; - - $(`.${className}`) - .children() - .each(function() { - /* eslint-disable no-invalid-this */ - if (this.id !== 'remoteVideos') { - width += $(this).outerWidth(); - } - /* eslint-enable no-invalid-this */ - }); - - return width; - }, - - /** - Calculate the thumbnail size in order to fit all the thumnails in passed - * dimensions. - * NOTE: Here we assume that the remote and local thumbnails are with the - * same height. - * @param {int} availableWidth the maximum width for all thumbnails - * @param {int} availableHeight the maximum height for all thumbnails - * @returns {{localVideo, remoteVideo}} - */ - calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) { - /** - * Let: - * lW - width of the local thumbnail - * rW - width of the remote thumbnail - * h - the height of the thumbnails - * remoteRatio - width:height for the remote thumbnail - * localRatio - width:height for the local thumbnail - * remoteThumbsInRow - number of remote thumbnails in a row (we have - * only one local thumbnail) next to the local thumbnail. In vertical - * filmstrip mode, this will always be 0. - * - * Since the height for local thumbnail = height for remote thumbnail - * and we know the ratio (width:height) for the local and for the - * remote thumbnail we can find rW/lW: - * rW / remoteRatio = lW / localRatio then - - * remoteLocalWidthRatio = rW / lW = remoteRatio / localRatio - * and rW = lW * remoteRatio / localRatio = lW * remoteLocalWidthRatio - * And the total width for the thumbnails is: - * totalWidth = rW * remoteThumbsInRow + lW - * = lW * remoteLocalWidthRatio * remoteThumbsInRow + lW = - * lW * (remoteLocalWidthRatio * remoteThumbsInRow + 1) - * and the h = lW/localRatio - * - * In order to fit all the thumbails in the area defined by - * availableWidth * availableHeight we should check one of the - * following options: - * 1) if availableHeight == h - totalWidth should be less than - * availableWidth - * 2) if availableWidth == totalWidth - h should be less than - * availableHeight - * - * 1) or 2) will be true and we are going to use it to calculate all - * sizes. - * - * if 1) is true that means that - * availableHeight/h > availableWidth/totalWidth otherwise 2) is true - */ - - const remoteLocalWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO / interfaceConfig.LOCAL_THUMBNAIL_RATIO; - const lW = Math.min(availableWidth, availableHeight * interfaceConfig.LOCAL_THUMBNAIL_RATIO); - const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO; - - const remoteVideoWidth = lW * remoteLocalWidthRatio; - - let localVideo; - - if (interfaceConfig.VERTICAL_FILMSTRIP) { - localVideo = { - thumbWidth: remoteVideoWidth, - thumbHeight: h * remoteLocalWidthRatio - }; - } else { - localVideo = { - thumbWidth: lW, - thumbHeight: h - }; - } - - return { - localVideo, - remoteVideo: { - thumbWidth: remoteVideoWidth, - thumbHeight: h - } - }; - }, - - /** - * Calculates the size for thumbnails when in tile view layout. - * - * @returns {{localVideo, remoteVideo}} - */ - _calculateThumbnailSizeForTileView() { - const tileAspectRatio = 16 / 9; - - // The distance from the top and bottom of the screen, as set by CSS, to - // avoid overlapping UI elements. - const topBottomPadding = 200; - - // Minimum space to keep between the sides of the tiles and the sides - // of the window. - const sideMargins = 30 * 2; - - const state = APP.store.getState(); - - const viewWidth = document.body.clientWidth - sideMargins; - const viewHeight = document.body.clientHeight - topBottomPadding; - - const { - columns, - visibleRows - } = getTileViewGridDimensions(state, getMaxColumnCount()); - const initialWidth = viewWidth / columns; - const aspectRatioHeight = initialWidth / tileAspectRatio; - - const heightOfEach = Math.floor(Math.min( - aspectRatioHeight, - viewHeight / visibleRows - )); - const widthOfEach = Math.floor(tileAspectRatio * heightOfEach); - - return { - localVideo: { - thumbWidth: widthOfEach, - thumbHeight: heightOfEach - }, - remoteVideo: { - thumbWidth: widthOfEach, - thumbHeight: heightOfEach - } - }; - }, - - /** - * Resizes thumbnails - * @param local - * @param remote - * @param forceUpdate - * @returns {Promise} - */ - // eslint-disable-next-line max-params - resizeThumbnails(local, remote, forceUpdate = false) { - const state = APP.store.getState(); - - if (shouldDisplayTileView(state)) { - // The size of the side margins for each tile as set in CSS. - const sideMargins = 10 * 2; - const { columns, rows } = getTileViewGridDimensions(state, getMaxColumnCount()); - const hasOverflow = rows > columns; - - // Width is set so that the flex layout can automatically wrap - // tiles onto new rows. - this.filmstripRemoteVideos.css({ width: (local.thumbWidth * columns) + (columns * sideMargins) }); - this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow); - } else { - this.filmstripRemoteVideos.css('width', ''); - } - - const thumbs = this.getThumbs(!forceUpdate); + resizeThumbnailsForTileView(width, height, forceUpdate = false) { + const thumbs = this._getThumbs(!forceUpdate); + const avatarSize = height / 2; if (thumbs.localThumb) { - // eslint-disable-next-line no-shadow thumbs.localThumb.css({ - display: 'inline-block', - height: `${local.thumbHeight}px`, - 'min-height': `${local.thumbHeight}px`, - 'min-width': `${local.thumbWidth}px`, - width: `${local.thumbWidth}px` + 'padding-top': '', + height: `${height}px`, + 'min-height': `${height}px`, + 'min-width': `${width}px`, + width: `${width}px` }); - - const avatarSize = local.thumbHeight / 2; - - thumbs.localThumb.find('.avatar-container') - .height(avatarSize) - .width(avatarSize); } if (thumbs.remoteThumbs) { thumbs.remoteThumbs.css({ - display: 'inline-block', - height: `${remote.thumbHeight}px`, - 'min-height': `${remote.thumbHeight}px`, - 'min-width': `${remote.thumbWidth}px`, - width: `${remote.thumbWidth}px` - }); - - const avatarSize = remote.thumbHeight / 2; - - thumbs.remoteThumbs.find('.avatar-container') - .height(avatarSize) - .width(avatarSize); - } - - const currentLayout = getCurrentLayout(APP.store.getState()); - - // Let CSS take care of height in vertical filmstrip mode. - if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) { - $('#filmstripLocalVideo').css({ - // adds 4 px because of small video 2px border - width: `${local.thumbWidth + 4}px` - }); - } else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) { - this.filmstrip.css({ - // adds 4 px because of small video 2px border and 10px margin for the scroll - height: `${remote.thumbHeight + 14}px` + 'padding-top': '', + height: `${height}px`, + 'min-height': `${height}px`, + 'min-width': `${width}px`, + width: `${width}px` }); } - const { localThumb } = this.getThumbs(); - const height = localThumb ? localThumb.height() : 0; - const fontSize = UIUtil.getIndicatorFontSize(height); - - this.filmstrip.find('.indicator').css({ - 'font-size': `${fontSize}px` + $('.avatar-container').css({ + height: `${avatarSize}px`, + width: `${avatarSize}px` }); }, + /** + * Resizes thumbnails for horizontal view. + * + * @param {Object} dimensions - The new dimensions of the thumbnails. + * @param {boolean} forceUpdate + * @returns {void} + */ + resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) { + const thumbs = this._getThumbs(!forceUpdate); + + if (thumbs.localThumb) { + const { height, width } = local; + const avatarSize = height / 2; + + thumbs.localThumb.css({ + height: `${height}px`, + 'min-height': `${height}px`, + 'min-width': `${width}px`, + width: `${width}px` + }); + $('#localVideoContainer > .avatar-container').css({ + height: `${avatarSize}px`, + width: `${avatarSize}px` + }); + } + + if (thumbs.remoteThumbs) { + const { height, width } = remote; + const avatarSize = height / 2; + + thumbs.remoteThumbs.css({ + height: `${height}px`, + 'min-height': `${height}px`, + 'min-width': `${width}px`, + width: `${width}px` + }); + $('#filmstripRemoteVideosContainer > span > .avatar-container').css({ + height: `${avatarSize}px`, + width: `${avatarSize}px` + }); + } + }, + + /** + * Resizes thumbnails for vertical view. + * + * @returns {void} + */ + resizeThumbnailsForVerticalView() { + const thumbs = this._getThumbs(true); + + if (thumbs.localThumb) { + const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO; + + thumbs.localThumb.css({ + 'padding-top': `${heightToWidthPercent}%`, + width: '', + height: '', + 'min-width': '', + 'min-height': '' + }); + $('#localVideoContainer > .avatar-container').css({ + height: '50%', + width: `${heightToWidthPercent / 2}%` + }); + } + + if (thumbs.remoteThumbs) { + const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO; + + thumbs.remoteThumbs.css({ + 'padding-top': `${heightToWidthPercent}%`, + width: '', + height: '', + 'min-width': '', + 'min-height': '' + }); + $('#filmstripRemoteVideosContainer > span > .avatar-container').css({ + height: '50%', + width: `${heightToWidthPercent / 2}%` + }); + } + }, + /** * Returns thumbnails of the filmstrip * @param onlyVisible * @returns {object} thumbnails */ - getThumbs(onlyVisible = false) { + _getThumbs(onlyVisible = false) { let selector = 'span'; if (onlyVisible) { @@ -360,7 +167,7 @@ const Filmstrip = { } const localThumb = $('#localVideoContainer'); - const remoteThumbs = this.filmstripRemoteVideos.children(selector); + const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector); // Exclude the local video container if it has been hidden. if (localThumb.hasClass('hidden')) { diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index faa41e212..fff3d921d 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -490,9 +490,15 @@ export default class LargeVideoManager { show = APP.conference.isConnectionInterrupted(); } - const id = 'localConnectionMessage'; + const element = document.getElementById('localConnectionMessage'); - UIUtil.setVisible(id, show); + if (element) { + if (show) { + element.classList.add('show'); + } else { + element.classList.remove('show'); + } + } if (show) { // Avatar message conflicts with 'videoConnectionMessage', diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 43dc26e6a..f6d4d7e5f 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -33,6 +33,8 @@ export default class LocalVideo extends SmallVideo { this.streamEndedCallback = streamEndedCallback; this.container = this.createContainer(); this.$container = $(this.container); + this.isLocal = true; + this._setThumbnailSize(); this.updateDOMLocation(); this.localVideoId = null; @@ -40,10 +42,8 @@ export default class LocalVideo extends SmallVideo { if (!config.disableLocalVideoFlip) { this._buildContextMenu(); } - this.isLocal = true; this.emitter = emitter; - this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP - ? 'left top' : 'top center'; + this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center'; Object.defineProperty(this, 'id', { get() { diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 6e28a8294..248fcab2d 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -20,10 +20,7 @@ import { REMOTE_CONTROL_MENU_STATES, RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu'; -import { - LAYOUTS, - getCurrentLayout -} from '../../../react/features/video-layout'; +import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout'; /* eslint-enable no-unused-vars */ const logger = require('jitsi-meet-logger').getLogger(__filename); @@ -129,6 +126,7 @@ export default class RemoteVideo extends SmallVideo { addRemoteVideoContainer() { this.container = createContainer(this.videoSpanId); this.$container = $(this.container); + this._setThumbnailSize(); this.initBrowserSpecificProperties(); this.updateRemoteVideoMenu(); this.addAudioLevelIndicator(); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index c97ef8998..189768b8d 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -34,7 +34,6 @@ import { const logger = require('jitsi-meet-logger').getLogger(__filename); -import UIUtil from '../util/UIUtil'; import UIEvents from '../../../service/UI/UIEvents'; /** @@ -629,8 +628,7 @@ export default class SmallVideo { + participantId = { this.id } /> , thumbnail ); @@ -801,7 +799,8 @@ export default class SmallVideo { return; } - const iconSize = UIUtil.getIndicatorFontSize(); + const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {}; + const iconSize = NORMAL; const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED; const state = APP.store.getState(); const currentLayout = getCurrentLayout(state); @@ -830,11 +829,9 @@ export default class SmallVideo { connectionStatus = { this._connectionStatus } iconSize = { iconSize } isLocalVideo = { this.isLocal } - enableStatsDisplay - = { !interfaceConfig.filmStripOnly } + enableStatsDisplay = { !interfaceConfig.filmStripOnly } participantId = { this.id } - statsPopoverPosition - = { statsPopoverPosition } /> + statsPopoverPosition = { statsPopoverPosition } /> : null } { - if (videoElement) { - $(videoElement).show(); - } - }); + if (videoElement) { + $(videoElement).show(); + } this._updateLargeVideoIfDisplayed(resourceJid, true); }, @@ -378,37 +360,6 @@ const VideoLayout = { }); }, - /* - * Shows or hides the audio muted indicator over the local thumbnail video. - * @param {boolean} isMuted - */ - showLocalAudioIndicator(isMuted) { - localVideoThumbnail.showAudioIndicator(isMuted); - }, - - /** - * Resizes thumbnails. - */ - resizeThumbnails(forceUpdate = false, onComplete = null) { - const { localVideo, remoteVideo } = Filmstrip.calculateThumbnailSize(); - - Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate); - - if (shouldDisplayTileView(APP.store.getState())) { - const height = (localVideo && localVideo.thumbHeight) || (remoteVideo && remoteVideo.thumbnHeight) || 0; - const qualityLevel = getNearestReceiverVideoQualityLevel(height); - - APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel)); - } - - localVideoThumbnail && localVideoThumbnail.rerender(); - Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender()); - - if (onComplete && typeof onComplete === 'function') { - onComplete(); - } - }, - /** * On audio muted event. */ @@ -543,18 +494,6 @@ const VideoLayout = { } }, - /** - * Hides the connection indicator - * @param id - */ - hideConnectionIndicator(id) { - const remoteVideo = remoteVideos[id]; - - if (remoteVideo) { - remoteVideo.removeConnectionIndicator(); - } - }, - /** * Hides all the indicators */ @@ -586,8 +525,6 @@ const VideoLayout = { } else { logger.warn(`No remote video for ${id}`); } - - VideoLayout.resizeThumbnails(); }, onVideoTypeChanged(id, newVideoType) { @@ -623,12 +560,7 @@ const VideoLayout = { * * @param forceUpdate indicates that hidden thumbnails will be shown */ - resizeVideoArea( - forceUpdate = false, - animate = false) { - // Resize the thumbnails first. - this.resizeThumbnails(forceUpdate); - + resizeVideoArea(animate = false) { if (largeVideo) { largeVideo.updateContainerSize(); largeVideo.resize(animate); @@ -917,6 +849,10 @@ const VideoLayout = { refreshLayout() { localVideoThumbnail && localVideoThumbnail.updateDOMLocation(); VideoLayout.resizeVideoArea(); + + // Rerender the thumbnails since they are dependant on the layout because of the tooltip positioning. + localVideoThumbnail && localVideoThumbnail.rerender(); + Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender()); }, /** @@ -967,6 +903,13 @@ const VideoLayout = { if (this.isCurrentlyOnLarge(participantId)) { this.updateLargeVideo(participantId, force); } + }, + + /** + * Handles window resizes. + */ + onResize() { + VideoLayout.resizeVideoArea(); } }; diff --git a/react/features/base/conference/middleware.js b/react/features/base/conference/middleware.js index 0338a10de..3de1bbcb4 100644 --- a/react/features/base/conference/middleware.js +++ b/react/features/base/conference/middleware.js @@ -116,16 +116,12 @@ StateListenerRegistry.register( maxReceiverVideoQuality, preferredReceiverVideoQuality } = currentState; - const changedPreferredVideoQuality = preferredReceiverVideoQuality - !== previousState.preferredReceiverVideoQuality; - const changedMaxVideoQuality = maxReceiverVideoQuality - !== previousState.maxReceiverVideoQuality; + const changedPreferredVideoQuality + = preferredReceiverVideoQuality !== previousState.preferredReceiverVideoQuality; + const changedMaxVideoQuality = maxReceiverVideoQuality !== previousState.maxReceiverVideoQuality; if (changedPreferredVideoQuality || changedMaxVideoQuality) { - _setReceiverVideoConstraint( - conference, - preferredReceiverVideoQuality, - maxReceiverVideoQuality); + _setReceiverVideoConstraint(conference, preferredReceiverVideoQuality, maxReceiverVideoQuality); } }); diff --git a/react/features/base/react/components/web/BaseIndicator.js b/react/features/base/react/components/web/BaseIndicator.js index b6736e1a6..0c5f92922 100644 --- a/react/features/base/react/components/web/BaseIndicator.js +++ b/react/features/base/react/components/web/BaseIndicator.js @@ -72,7 +72,6 @@ class BaseIndicator extends Component { */ static defaultProps = { className: '', - iconSize: 13, id: '', tooltipPosition: 'top' }; @@ -95,8 +94,12 @@ class BaseIndicator extends Component { tooltipKey, tooltipPosition } = this.props; - const iconContainerClassName = `indicator-icon-container ${className}`; + const style = {}; + + if (iconSize) { + style.fontSize = iconSize; + } return (
@@ -110,7 +113,7 @@ class BaseIndicator extends Component { className = { iconClassName } id = { iconId } src = { icon } - style = {{ fontSize: iconSize }} /> + style = { style } />
diff --git a/react/features/base/responsive-ui/actionTypes.js b/react/features/base/responsive-ui/actionTypes.js index 4212381a5..a9920f393 100644 --- a/react/features/base/responsive-ui/actionTypes.js +++ b/react/features/base/responsive-ui/actionTypes.js @@ -1,3 +1,12 @@ +/** + * The type of (redux) action which indicates that the client window has been resized. + * + * { + * type: CLIENT_RESIZED + * } + */ +export const CLIENT_RESIZED = 'CLIENT_RESIZED'; + /** * The type of (redux) action which sets the aspect ratio of the app's user * interface. diff --git a/react/features/base/responsive-ui/actions.js b/react/features/base/responsive-ui/actions.js index 3bc3ca55a..aeb2767ba 100644 --- a/react/features/base/responsive-ui/actions.js +++ b/react/features/base/responsive-ui/actions.js @@ -1,10 +1,10 @@ // @flow -import { SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes'; -import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants'; - import type { Dispatch } from 'redux'; +import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes'; +import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants'; + /** * Size threshold for determining if we are in reduced UI mode or not. * @@ -16,6 +16,21 @@ import type { Dispatch } from 'redux'; */ const REDUCED_UI_THRESHOLD = 300; +/** + * Indicates a resize of the window. + * + * @param {number} clientWidth - The width of the window. + * @param {number} clientHeight - The height of the window. + * @returns {Object} + */ +export function clientResized(clientWidth: number, clientHeight: number) { + return { + type: CLIENT_RESIZED, + clientHeight, + clientWidth + }; +} + /** * Sets the aspect ratio of the app's user interface based on specific width and * height. diff --git a/react/features/base/responsive-ui/middleware.web.js b/react/features/base/responsive-ui/middleware.web.js index e69de29bb..9d5109e56 100644 --- a/react/features/base/responsive-ui/middleware.web.js +++ b/react/features/base/responsive-ui/middleware.web.js @@ -0,0 +1,69 @@ +// @flow + +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app'; +import { MiddlewareRegistry } from '../../base/redux'; + +import { clientResized } from './actions'; + +/** + * Dimensions change handler. + */ +let handler; + +/** + * Middleware that handles window dimension changes. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + const result = next(action); + + switch (action.type) { + case APP_WILL_UNMOUNT: { + _appWillUnmount(); + break; + } + case APP_WILL_MOUNT: + _appWillMount(store); + break; + + } + + return result; +}); + +/** + * Notifies this feature that the action {@link APP_WILL_MOUNT} is being + * dispatched within a specific redux {@code store}. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @private + * @returns {void} + */ +function _appWillMount(store) { + handler = () => { + const { + innerHeight, + innerWidth + } = window; + + store.dispatch(clientResized(innerWidth, innerHeight)); + }; + + window.addEventListener('resize', handler); +} + +/** + * Notifies this feature that the action {@link APP_WILL_UNMOUNT} is being + * dispatched within a specific redux {@code store}. + * + * @private + * @returns {void} + */ +function _appWillUnmount() { + window.removeEventListener('resize', handler); + + handler = undefined; +} diff --git a/react/features/base/responsive-ui/reducer.js b/react/features/base/responsive-ui/reducer.js index e5f711074..59f2d5ed2 100644 --- a/react/features/base/responsive-ui/reducer.js +++ b/react/features/base/responsive-ui/reducer.js @@ -2,19 +2,34 @@ import { ReducerRegistry, set } from '../redux'; -import { SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes'; +import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes'; import { ASPECT_RATIO_NARROW } from './constants'; +const { + innerHeight = 0, + innerWidth = 0 +} = window; + /** * The default/initial redux state of the feature base/responsive-ui. */ const DEFAULT_STATE = { aspectRatio: ASPECT_RATIO_NARROW, + clientHeight: innerHeight, + clientWidth: innerWidth, reducedUI: false }; ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => { switch (action.type) { + case CLIENT_RESIZED: { + + return { + ...state, + clientWidth: action.clientWidth, + clientHeight: action.clientHeight + }; + } case SET_ASPECT_RATIO: return set(state, 'aspectRatio', action.aspectRatio); diff --git a/react/features/filmstrip/actionTypes.js b/react/features/filmstrip/actionTypes.js index c70f801d9..509ecc09e 100644 --- a/react/features/filmstrip/actionTypes.js +++ b/react/features/filmstrip/actionTypes.js @@ -28,3 +28,23 @@ export const SET_FILMSTRIP_HOVERED = 'SET_FILMSTRIP_HOVERED'; * } */ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE'; + +/** + * The type of (redux) action which sets the dimensions of the tile view grid. + * + * { + * type: SET_TILE_VIEW_DIMENSIONS, + * dimensions: Object + * } + */ +export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS'; + +/** + * The type of (redux) action which sets the dimensions of the thumbnails in horizontal view. + * + * { + * type: SET_HORIZONTAL_VIEW_DIMENSIONS, + * dimensions: Object + * } + */ +export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS'; diff --git a/react/features/filmstrip/actions.js b/react/features/filmstrip/actions.native.js similarity index 100% rename from react/features/filmstrip/actions.js rename to react/features/filmstrip/actions.native.js diff --git a/react/features/filmstrip/actions.web.js b/react/features/filmstrip/actions.web.js new file mode 100644 index 000000000..67df96d9e --- /dev/null +++ b/react/features/filmstrip/actions.web.js @@ -0,0 +1,54 @@ +// @flow + +import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes'; +import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions'; + +/** + * The size of the side margins for each tile as set in CSS. + */ +const TILE_VIEW_SIDE_MARGINS = 10 * 2; + +/** + * Sets the dimensions of the tile view grid. + * + * @param {Object} dimensions - Whether the filmstrip is visible. + * @param {Object} windowSize - The size of the window. + * @returns {{ + * type: SET_TILE_VIEW_DIMENSIONS, + * dimensions: Object + * }} + */ +export function setTileViewDimensions(dimensions: Object, windowSize: Object) { + const thumbnailSize = calculateThumbnailSizeForTileView({ + ...dimensions, + ...windowSize + }); + const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width); + + return { + type: SET_TILE_VIEW_DIMENSIONS, + dimensions: { + gridDimensions: dimensions, + thumbnailSize, + filmstripWidth + } + }; +} + +/** + * Sets the dimensions of the thumbnails in horizontal view. + * + * @param {number} clientHeight - The height of the window. + * @returns {{ + * type: SET_HORIZONTAL_VIEW_DIMENSIONS, + * dimensions: Object + * }} + */ +export function setHorizontalViewDimensions(clientHeight: number = 0) { + return { + type: SET_HORIZONTAL_VIEW_DIMENSIONS, + dimensions: calculateThumbnailSizeForHorizontalView(clientHeight) + }; +} + +export * from './actions.native'; diff --git a/react/features/filmstrip/components/web/AudioMutedIndicator.js b/react/features/filmstrip/components/web/AudioMutedIndicator.js index c9d1eeceb..1f7f49303 100644 --- a/react/features/filmstrip/components/web/AudioMutedIndicator.js +++ b/react/features/filmstrip/components/web/AudioMutedIndicator.js @@ -34,6 +34,7 @@ class AudioMutedIndicator extends Component { className = 'audioMuted toolbar-icon' icon = { IconMicDisabled } iconId = 'mic-disabled' + iconSize = { 13 } tooltipKey = 'videothumbnail.mute' tooltipPosition = { this.props.tooltipPosition } /> ); diff --git a/react/features/filmstrip/components/web/Filmstrip.js b/react/features/filmstrip/components/web/Filmstrip.js index 982393634..a81dbd89e 100644 --- a/react/features/filmstrip/components/web/Filmstrip.js +++ b/react/features/filmstrip/components/web/Filmstrip.js @@ -16,6 +16,8 @@ import { dockToolbox } from '../../../toolbox'; import { setFilmstripHovered, setFilmstripVisible } from '../../actions'; import { shouldRemoteVideosBeVisible } from '../../functions'; +import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; + import Toolbar from './Toolbar'; declare var APP: Object; @@ -31,17 +33,37 @@ type Props = { */ _className: string, + /** + * The current layout of the filmstrip. + */ + _currentLayout: string, + + /** + * The number of columns in tile view. + */ + _columns: number, + /** * Whether the UI/UX is filmstrip-only. */ _filmstripOnly: boolean, + /** + * The width of the filmstrip. + */ + _filmstripWidth: number, + /** * Whether or not remote videos are currently being hovered over. Hover * handling is currently being handled detected outside of react. */ _hovered: boolean, + /** + * The number of rows in tile view. + */ + _rows: number, + /** * Additional CSS class names to add to the container of all the thumbnails. */ @@ -87,8 +109,7 @@ class Filmstrip extends Component { // also works around an issue where mouseout and then a mouseover event // is fired when hovering over remote thumbnails, which are not yet in // react. - this._notifyOfHoveredStateUpdate - = _.debounce(this._notifyOfHoveredStateUpdate, 100); + this._notifyOfHoveredStateUpdate = _.debounce(this._notifyOfHoveredStateUpdate, 100); // Cache the current hovered state for _updateHoveredState to always // send the last known hovered state. @@ -97,10 +118,8 @@ class Filmstrip extends Component { // Bind event handlers so they are only bound once for every instance. this._onMouseOut = this._onMouseOut.bind(this); this._onMouseOver = this._onMouseOver.bind(this); - this._onShortcutToggleFilmstrip - = this._onShortcutToggleFilmstrip.bind(this); - this._onToolbarToggleFilmstrip - = this._onToolbarToggleFilmstrip.bind(this); + this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this); + this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this); } /** @@ -142,13 +161,36 @@ class Filmstrip extends Component { // will get updated without replacing the DOM. If the known DOM gets // modified, then the views will get blown away. + const remoteVideosStyle = { }; + const filmstripRemoteVideosContainerStyle = {}; + let remoteVideoContainerClassName = 'remote-videos-container'; + + switch (this.props._currentLayout) { + case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: + // Adding 8px for the 2px margins and 2px borders on the left and right. Also adding 7px for the scrollbar. + remoteVideosStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 15; + break; + case LAYOUTS.TILE_VIEW: { + // The size of the side margins for each tile as set in CSS. + const { _columns, _rows, _filmstripWidth } = this.props; + + if (_rows > _columns) { + remoteVideoContainerClassName += ' has-overflow'; + } + + filmstripRemoteVideosContainerStyle.width = _filmstripWidth; + break; + } + } + return (
{ this.props._filmstripOnly ? : this._renderToggleButton() }
+ id = 'remoteVideos' + style = { remoteVideosStyle }>
{ * thumbnails resize instead of causing overflow. */}
+ onMouseOver = { this._onMouseOver } + style = { filmstripRemoteVideosContainerStyle }>
@@ -301,20 +344,24 @@ class Filmstrip extends Component { function _mapStateToProps(state) { const { hovered, visible } = state['features/filmstrip']; const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly); - const reduceHeight = !isFilmstripOnly - && state['features/toolbox'].visible - && interfaceConfig.TOOLBAR_BUTTONS.length; + const reduceHeight + = !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length; const remoteVideosVisible = shouldRemoteVideosBeVisible(state); - const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${ - reduceHeight ? 'reduce-height' : ''}`.trim(); + const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`.trim(); const videosClassName = `filmstrip__videos${ isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${ visible ? '' : ' hidden'}`; + const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions; + return { _className: className, + _columns: gridDimensions.columns, + _currentLayout: getCurrentLayout(state), _filmstripOnly: isFilmstripOnly, + _filmstripWidth: filmstripWidth, _hovered: hovered, + _rows: gridDimensions.rows, _videosClassName: videosClassName, _visible: visible }; diff --git a/react/features/filmstrip/components/web/ModeratorIndicator.js b/react/features/filmstrip/components/web/ModeratorIndicator.js index 1356dc407..0f6b88baf 100644 --- a/react/features/filmstrip/components/web/ModeratorIndicator.js +++ b/react/features/filmstrip/components/web/ModeratorIndicator.js @@ -34,6 +34,7 @@ class ModeratorIndicator extends Component {
diff --git a/react/features/filmstrip/components/web/VideoMutedIndicator.js b/react/features/filmstrip/components/web/VideoMutedIndicator.js index bf0e2d136..54775a2ed 100644 --- a/react/features/filmstrip/components/web/VideoMutedIndicator.js +++ b/react/features/filmstrip/components/web/VideoMutedIndicator.js @@ -33,6 +33,7 @@ class VideoMutedIndicator extends Component { className = 'videoMuted toolbar-icon' icon = { IconCameraDisabled } iconId = 'camera-disabled' + iconSize = { 13 } tooltipKey = 'videothumbnail.videomute' tooltipPosition = { this.props.tooltipPosition } /> ); diff --git a/react/features/filmstrip/constants.js b/react/features/filmstrip/constants.js index 6aa593081..147ff410a 100644 --- a/react/features/filmstrip/constants.js +++ b/react/features/filmstrip/constants.js @@ -4,3 +4,8 @@ * The height of the filmstrip in narrow aspect ratio, or width in wide. */ export const FILMSTRIP_SIZE = 90; + +/** + * The aspect ratio of a tile in tile view. + */ +export const TILE_ASPECT_RATIO = 16 / 9; diff --git a/react/features/filmstrip/functions.web.js b/react/features/filmstrip/functions.web.js index 622315bc0..bcd8fe96e 100644 --- a/react/features/filmstrip/functions.web.js +++ b/react/features/filmstrip/functions.web.js @@ -6,6 +6,8 @@ import { } from '../base/participants'; import { toState } from '../base/redux'; +import { TILE_ASPECT_RATIO } from './constants'; + declare var interfaceConfig: Object; /** @@ -59,3 +61,58 @@ export function shouldRemoteVideosBeVisible(state: Object) { || state['features/base/config'].disable1On1Mode); } + +/** + * Calculates the size for thumbnails when in horizontal view layout. + * + * @param {number} clientHeight - The height of the app window. + * @returns {{local: {height, width}, remote: {height, width}}} + */ +export function calculateThumbnailSizeForHorizontalView(clientHeight: number = 0) { + const topBottomMargin = 15; + const availableHeight = Math.min(clientHeight, (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + topBottomMargin); + const height = availableHeight - topBottomMargin; + + return { + local: { + height, + width: Math.floor(interfaceConfig.LOCAL_THUMBNAIL_RATIO * height) + }, + remote: { + height, + width: Math.floor(interfaceConfig.REMOTE_THUMBNAIL_RATIO * height) + } + }; +} + +/** + * Calculates the size for thumbnails when in tile view layout. + * + * @param {Object} dimensions - The desired dimensions of the tile view grid. + * @returns {{height, width}} + */ +export function calculateThumbnailSizeForTileView({ + columns, + visibleRows, + clientWidth, + clientHeight +}: Object) { + // The distance from the top and bottom of the screen, as set by CSS, to + // avoid overlapping UI elements. + const topBottomPadding = 200; + + // Minimum space to keep between the sides of the tiles and the sides + // of the window. + const sideMargins = 30 * 2; + const viewWidth = clientWidth - sideMargins; + const viewHeight = clientHeight - topBottomPadding; + const initialWidth = viewWidth / columns; + const aspectRatioHeight = initialWidth / TILE_ASPECT_RATIO; + const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows)); + const width = Math.floor(TILE_ASPECT_RATIO * height); + + return { + height, + width + }; +} diff --git a/react/features/filmstrip/index.js b/react/features/filmstrip/index.js index 0bacc21c5..7689c0d79 100644 --- a/react/features/filmstrip/index.js +++ b/react/features/filmstrip/index.js @@ -5,3 +5,5 @@ export * from './constants'; export * from './functions'; import './reducer'; +import './subscriber'; +import './middleware'; diff --git a/react/features/filmstrip/middleware.native.js b/react/features/filmstrip/middleware.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/filmstrip/middleware.web.js b/react/features/filmstrip/middleware.web.js new file mode 100644 index 000000000..d7bbd2e79 --- /dev/null +++ b/react/features/filmstrip/middleware.web.js @@ -0,0 +1,74 @@ +// @flow + +import { getNearestReceiverVideoQualityLevel, setMaxReceiverVideoQuality } from '../base/conference'; +import { MiddlewareRegistry } from '../base/redux'; +import { CLIENT_RESIZED } from '../base/responsive-ui'; +import Filmstrip from '../../../modules/UI/videolayout/Filmstrip'; +import { + getCurrentLayout, + LAYOUTS, + shouldDisplayTileView +} from '../video-layout'; + +import { setHorizontalViewDimensions, setTileViewDimensions } from './actions'; +import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes'; + +/** + * The middleware of the feature Filmstrip. + */ +MiddlewareRegistry.register(store => next => action => { + const result = next(action); + + switch (action.type) { + case CLIENT_RESIZED: { + const state = store.getState(); + const layout = getCurrentLayout(state); + + switch (layout) { + case LAYOUTS.TILE_VIEW: { + const { gridDimensions } = state['features/filmstrip'].tileViewDimensions; + const { clientHeight, clientWidth } = state['features/base/responsive-ui']; + + store.dispatch(setTileViewDimensions(gridDimensions, { + clientHeight, + clientWidth + })); + break; + } + case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: + store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight)); + break; + } + break; + } + case SET_TILE_VIEW_DIMENSIONS: { + const state = store.getState(); + + if (shouldDisplayTileView(state)) { + const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize; + const qualityLevel = getNearestReceiverVideoQualityLevel(height); + + store.dispatch(setMaxReceiverVideoQuality(qualityLevel)); + + // Once the thumbnails are reactified this should be moved there too. + Filmstrip.resizeThumbnailsForTileView(width, height, true); + } + break; + } + case SET_HORIZONTAL_VIEW_DIMENSIONS: { + const state = store.getState(); + + if (getCurrentLayout(state) === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) { + const { horizontalViewDimensions = {} } = state['features/filmstrip']; + + // Once the thumbnails are reactified this should be moved there too. + Filmstrip.resizeThumbnailsForHorizontalView(horizontalViewDimensions, true); + } + + break; + } + } + + return result; +}); + diff --git a/react/features/filmstrip/reducer.js b/react/features/filmstrip/reducer.js index 7e4cc11ce..b4c796c94 100644 --- a/react/features/filmstrip/reducer.js +++ b/react/features/filmstrip/reducer.js @@ -5,7 +5,9 @@ import { ReducerRegistry } from '../base/redux'; import { SET_FILMSTRIP_ENABLED, SET_FILMSTRIP_HOVERED, - SET_FILMSTRIP_VISIBLE + SET_FILMSTRIP_VISIBLE, + SET_HORIZONTAL_VIEW_DIMENSIONS, + SET_TILE_VIEW_DIMENSIONS } from './actionTypes'; const DEFAULT_STATE = { @@ -17,6 +19,22 @@ const DEFAULT_STATE = { */ enabled: true, + /** + * The horizontal view dimensions. + * + * @public + * @type {Object} + */ + horizontalViewDimensions: {}, + + /** + * The tile view dimensions. + * + * @public + * @type {Object} + */ + tileViewDimensions: {}, + /** * The indicator which determines whether the {@link Filmstrip} is visible. * @@ -55,6 +73,17 @@ ReducerRegistry.register( ...state, visible: action.visible }; + + case SET_HORIZONTAL_VIEW_DIMENSIONS: + return { + ...state, + horizontalViewDimensions: action.dimensions + }; + case SET_TILE_VIEW_DIMENSIONS: + return { + ...state, + tileViewDimensions: action.dimensions + }; } return state; diff --git a/react/features/filmstrip/subscriber.native.js b/react/features/filmstrip/subscriber.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/filmstrip/subscriber.web.js b/react/features/filmstrip/subscriber.web.js new file mode 100644 index 000000000..e1f198a2e --- /dev/null +++ b/react/features/filmstrip/subscriber.web.js @@ -0,0 +1,58 @@ +// @flow + +import { StateListenerRegistry, equals } from '../base/redux'; +import Filmstrip from '../../../modules/UI/videolayout/Filmstrip'; +import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout'; + +import { setHorizontalViewDimensions, setTileViewDimensions } from './actions'; + +/** + * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles. + */ +StateListenerRegistry.register( + /* selector */ state => state['features/base/participants'].length, + /* listener */ (numberOfParticipants, store) => { + const state = store.getState(); + + if (shouldDisplayTileView(state)) { + const gridDimensions = getTileViewGridDimensions(state['features/base/participants'].length); + const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions; + const { clientHeight, clientWidth } = state['features/base/responsive-ui']; + + if (!equals(gridDimensions, oldGridDimensions)) { + store.dispatch(setTileViewDimensions(gridDimensions, { + clientHeight, + clientWidth + })); + } + } + }); + +/** + * Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view. + */ +StateListenerRegistry.register( + /* selector */ state => getCurrentLayout(state), + /* listener */ (layout, store) => { + const state = store.getState(); + + switch (layout) { + case LAYOUTS.TILE_VIEW: { + const { clientHeight, clientWidth } = state['features/base/responsive-ui']; + + store.dispatch(setTileViewDimensions( + getTileViewGridDimensions(state['features/base/participants'].length), { + clientHeight, + clientWidth + })); + break; + } + case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: + store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight)); + break; + case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: + // Once the thumbnails are reactified this should be moved there too. + Filmstrip.resizeThumbnailsForVerticalView(); + break; + } + }); diff --git a/react/features/video-layout/functions.js b/react/features/video-layout/functions.js index 95e4322e7..e9cf7040f 100644 --- a/react/features/video-layout/functions.js +++ b/react/features/video-layout/functions.js @@ -39,25 +39,20 @@ export function getMaxColumnCount() { * equal count of tiles for height and width, until maxColumn is reached in * which rows will be added but no more columns. * - * @param {Object} state - The redux state. + * @param {number} numberOfParticipants - The number of participants including the fake participants. * @param {number} maxColumns - The maximum number of columns that can be * displayed. * @returns {Object} An object is return with the desired number of columns, * rows, and visible rows (the rest should overflow) for the tile view layout. */ -export function getTileViewGridDimensions(state: Object, maxColumns: number) { - // Purposefully include all participants, which includes fake participants - // that should show a thumbnail. - const potentialThumbnails = state['features/base/participants'].length; - - const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails)); +export function getTileViewGridDimensions(numberOfParticipants: number, maxColumns: number = getMaxColumnCount()) { + const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants)); const columns = Math.min(columnsToMaintainASquare, maxColumns); - const rows = Math.ceil(potentialThumbnails / columns); + const rows = Math.ceil(numberOfParticipants / columns); const visibleRows = Math.min(maxColumns, rows); return { columns, - rows, visibleRows }; }