ref(Filmstrip): Optimize resizes. (#4992)

* ref(Filmstrip): Optimize resizes.

* fix(thumbnails): resize.

* fix(thumbnails): Issue with height 0, width 0.

* doc(Filmstrip): Improve JSDoc.
This commit is contained in:
Hristo Terezov 2020-01-24 16:28:47 +00:00 committed by GitHub
parent ca9ca04d0f
commit 31d9fb12c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 761 additions and 715 deletions

View File

@ -31,13 +31,6 @@
display: inline-block !important;
}
/**
* Shows as a list item
**/
.show-list-item {
display: list-item !important;
}
/**
* Shows a flex element.
*/

View File

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

View File

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

View File

@ -1,5 +1,5 @@
.filmstrip__videos .videocontainer {
display: none;
display: inline-block;
position: relative;
background-size: contain;
border: $thumbnailVideoBorder solid transparent;

View File

@ -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 **/

View File

@ -56,6 +56,10 @@
transform: translate3d(0, 0, 0);
}
.indicator-icon-container {
display: inline-block;
}
.indicator-container {
float: none;
}

View File

@ -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.
*/

View File

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

View File

@ -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 <tt>true</tt> to show or <tt>false</tt> 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;
}
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id }
size = { this.$avatar().width() } />
participantId = { this.id } />
</Provider>,
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 }
<RaisedHandIndicator
iconSize = { iconSize }
@ -936,4 +933,67 @@ export default class SmallVideo {
this._popoverIsHovered = popoverIsHovered;
this.updateView();
}
/**
* Sets the size of the thumbnail.
*/
_setThumbnailSize() {
const layout = getCurrentLayout(APP.store.getState());
const heightToWidthPercent = 100
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
this.$avatar().css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const state = APP.store.getState();
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
const size = this.isLocal ? local : remote;
if (typeof size !== 'undefined') {
const { height, width } = size;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
case LAYOUTS.TILE_VIEW: {
const state = APP.store.getState();
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
'min-height': `${height}px`,
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
}
}
}

View File

@ -1,10 +1,6 @@
/* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import {
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality
} from '../../../react/features/base/conference';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
@ -14,13 +10,9 @@ import {
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
shouldDisplayTileView
} from '../../../react/features/video-layout';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
import Filmstrip from './Filmstrip';
import UIEvents from '../../../service/UI/UIEvents';
import RemoteVideo from './RemoteVideo';
@ -87,11 +79,6 @@ const VideoLayout = {
// sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
this.resizeThumbnails(true);
this.registerListeners();
},
@ -331,8 +318,6 @@ const VideoLayout = {
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
}
VideoLayout.resizeThumbnails(true);
// Initialize the view
remoteVideo.updateView();
},
@ -340,12 +325,9 @@ const VideoLayout = {
// FIXME: what does this do???
remoteVideoActive(videoElement, resourceJid) {
logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails(
false, () => {
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();
}
};

View File

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

View File

@ -72,7 +72,6 @@ class BaseIndicator extends Component<Props> {
*/
static defaultProps = {
className: '',
iconSize: 13,
id: '',
tooltipPosition: 'top'
};
@ -95,8 +94,12 @@ class BaseIndicator extends Component<Props> {
tooltipKey,
tooltipPosition
} = this.props;
const iconContainerClassName = `indicator-icon-container ${className}`;
const style = {};
if (iconSize) {
style.fontSize = iconSize;
}
return (
<div className = 'indicator-container'>
@ -110,7 +113,7 @@ class BaseIndicator extends Component<Props> {
className = { iconClassName }
id = { iconId }
src = { icon }
style = {{ fontSize: iconSize }} />
style = { style } />
</span>
</Tooltip>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ class AudioMutedIndicator extends Component<Props> {
className = 'audioMuted toolbar-icon'
icon = { IconMicDisabled }
iconId = 'mic-disabled'
iconSize = { 13 }
tooltipKey = 'videothumbnail.mute'
tooltipPosition = { this.props.tooltipPosition } />
);

View File

@ -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 <Props> {
// 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 <Props> {
// 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 <Props> {
// 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 (
<div className = { `filmstrip ${this.props._className}` }>
{ this.props._filmstripOnly
? <Toolbar /> : this._renderToggleButton() }
<div
className = { this.props._videosClassName }
id = 'remoteVideos'>
id = 'remoteVideos'
style = { remoteVideosStyle }>
<div
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'
@ -165,10 +207,11 @@ class Filmstrip extends Component <Props> {
* thumbnails resize instead of causing overflow.
*/}
<div
className = 'remote-videos-container'
className = { remoteVideoContainerClassName }
id = 'filmstripRemoteVideosContainer'
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
onMouseOver = { this._onMouseOver }
style = { filmstripRemoteVideosContainerStyle }>
<div id = 'localVideoTileViewContainer' />
</div>
</div>
@ -301,20 +344,24 @@ class Filmstrip extends Component <Props> {
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
};

View File

@ -34,6 +34,7 @@ class ModeratorIndicator extends Component<Props> {
<BaseIndicator
className = 'focusindicator toolbar-icon'
icon = { IconModerator }
iconSize = { 13 }
tooltipKey = 'videothumbnail.moderator'
tooltipPosition = { this.props.tooltipPosition } />
</div>

View File

@ -33,6 +33,7 @@ class VideoMutedIndicator extends Component<Props> {
className = 'videoMuted toolbar-icon'
icon = { IconCameraDisabled }
iconId = 'camera-disabled'
iconSize = { 13 }
tooltipKey = 'videothumbnail.videomute'
tooltipPosition = { this.props.tooltipPosition } />
);

View File

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

View File

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

View File

@ -5,3 +5,5 @@ export * from './constants';
export * from './functions';
import './reducer';
import './subscriber';
import './middleware';

View File

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

View File

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

View File

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

View File

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