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; display: inline-block !important;
} }
/**
* Shows as a list item
**/
.show-list-item {
display: list-item !important;
}
/** /**
* Shows a flex element. * Shows a flex element.
*/ */

View File

@ -173,7 +173,9 @@
&__hoverOverlay { &__hoverOverlay {
background: rgba(0,0,0,.6); background: rgba(0,0,0,.6);
border-radius: $borderRadius; border-radius: $borderRadius;
position: relative; position: absolute;
top: 0px;
left: 0px;
width: 100%; width: 100%;
height: 100%; height: 100%;
visibility: hidden; visibility: hidden;
@ -526,13 +528,16 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
height: 50%; height: 50%;
overflow: hidden;
width: auto; width: auto;
overflow: hidden;
.userAvatar { .userAvatar {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
width: 100%; width: 100%;
top: 0px;
left: 0px;
position: absolute;
} }
} }
@ -555,6 +560,9 @@
} }
.sharedVideoAvatar { .sharedVideoAvatar {
position: absolute;
left: 0px;
top: 0px;
height: 100%; height: 100%;
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;

View File

@ -55,6 +55,7 @@
&#filmstripLocalVideo { &#filmstripLocalVideo {
align-self: flex-end; align-self: flex-end;
display: block; display: block;
margin-bottom: 8px;
} }
&.hidden { &.hidden {
@ -108,6 +109,7 @@
*/ */
padding: 1px 0; padding: 1px 0;
overflow-y: hidden; overflow-y: hidden;
overflow-x: scroll;
} }
} }

View File

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

View File

@ -25,6 +25,7 @@
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
height: 100%; height: 100%;
width: 100%;
padding: ($desktopAppDragBarHeight - 5px) 5px 10px; padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
/** /**
* fixed positioning is necessary for remote menus and tooltips to pop * fixed positioning is necessary for remote menus and tooltips to pop
@ -48,7 +49,6 @@
.filmstrip__videos { .filmstrip__videos {
@extend %align-right; @extend %align-right;
bottom: 0; bottom: 0;
overflow: visible !important;
padding: 0; padding: 0;
position:relative; position:relative;
right: 0; right: 0;
@ -67,6 +67,7 @@
border: $thumbnailsBorder solid transparent; border: $thumbnailsBorder solid transparent;
padding-left: 0; padding-left: 0;
transition: right 2s; transition: right 2s;
width: 100%;
} }
} }
@ -80,6 +81,16 @@
flex-direction: column-reverse; flex-direction: column-reverse;
height: auto; height: auto;
justify-content: flex-start; justify-content: flex-start;
#filmstripLocalVideoThumbnail {
width: calc(100% - 15px);
.videocontainer {
height: 0px;
width: 100%;
}
}
} }
/** /**
@ -96,18 +107,21 @@
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column-reverse;
height: auto; height: auto;
justify-content: flex-end; overflow-x: hidden;
overflow-y: scroll;
#filmstripRemoteVideosContainer { #filmstripRemoteVideosContainer {
@include minHWAutoFix();
flex-direction: column-reverse; flex-direction: column-reverse;
/** overflow: visible;
* Add padding as a hack for Firefox not to show scrollbars when width: calc(100% - 8px); // 8px for margin + border of the thumbnails
* unnecessary.
*/ .videocontainer {
padding: 1px 0; height: 0px;
overflow-x: hidden; 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 **/ /** Firefox detection hack **/
@-moz-document url-prefix() { @-moz-document url-prefix() {
@include undoColumnReverseVideos(); @include undoColumnReverseVideos();
@include filmstripSizeWithoutScroll();
} }
/** Edge detection hack **/ /** Edge detection hack **/

View File

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

View File

@ -11,7 +11,6 @@ import EtherpadManager from './etherpad/Etherpad';
import SharedVideoManager from './shared_video/SharedVideo'; import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from './videolayout/VideoLayout'; import VideoLayout from './videolayout/VideoLayout';
import Filmstrip from './videolayout/Filmstrip';
import { getLocalParticipant } from '../../react/features/base/participants'; import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat'; import { toggleChat } from '../../react/features/chat';
@ -158,8 +157,6 @@ UI.start = function() {
// Set the defaults for prompt dialogs. // Set the defaults for prompt dialogs.
$.prompt.setDefaults({ persistent: false }); $.prompt.setDefaults({ persistent: false });
Filmstrip.init(eventEmitter);
VideoLayout.init(eventEmitter); VideoLayout.init(eventEmitter);
if (!interfaceConfig.filmStripOnly) { if (!interfaceConfig.filmStripOnly) {
VideoLayout.initLargeVideo(); VideoLayout.initLargeVideo();
@ -202,7 +199,7 @@ UI.bindEvents = () => {
* *
*/ */
function onResize() { function onResize() {
VideoLayout.resizeVideoArea(); VideoLayout.onResize();
} }
// Resize and reposition videos in full screen mode. // Resize and reposition videos in full screen mode.
@ -353,12 +350,6 @@ UI.toggleFilmstrip = function() {
APP.store.dispatch(setFilmstripVisible(!visible)); 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. * Toggles the visibility of the chat panel.
*/ */

View File

@ -17,17 +17,16 @@ export default class SharedVideoThumb extends SmallVideo {
constructor(participant, videoType, VideoLayout) { constructor(participant, videoType, VideoLayout) {
super(VideoLayout); super(VideoLayout);
this.id = participant.id; this.id = participant.id;
this.isLocal = false;
this.url = participant.id; this.url = participant.id;
this.setVideoType(videoType); this.setVideoType(videoType);
this.videoSpanId = 'sharedVideoContainer'; this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId); this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container); this.$container = $(this.container);
this._setThumbnailSize();
this.bindHoverHandler(); this.bindHoverHandler();
this.isVideoMuted = true; this.isVideoMuted = true;
this.updateDisplayName(); this.updateDisplayName();
this.container.onclick = this._onContainerClick; this.container.onclick = this._onContainerClick;
} }

View File

@ -1,22 +1,4 @@
/* global $, interfaceConfig */ /* global $ */
/**
* 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
};
/** /**
* Created by hristo on 12/22/14. * Created by hristo on 12/22/14.
@ -30,32 +12,6 @@ const UIUtil = {
return window.innerWidth; 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. * Escapes the given text.
*/ */
@ -64,27 +20,6 @@ const UIUtil = {
.html(); .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. * Inserts given child element as the first one into the container.
* @param container the container to which new child element will be added * @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. * Redirects to a given URL.
* *
@ -200,17 +60,6 @@ const UIUtil = {
|| document.msFullscreenElement); || 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 * 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 * will be null if the "display" property of the element or any of its
@ -220,100 +69,6 @@ const UIUtil = {
*/ */
isVisible(el) { isVisible(el) {
return el.offsetParent !== null; 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 */ /* global $, APP, interfaceConfig */
import { import { isFilmstripVisible } from '../../../react/features/filmstrip';
LAYOUTS,
getCurrentLayout,
getMaxColumnCount,
getTileViewGridDimensions,
shouldDisplayTileView
} from '../../../react/features/video-layout';
import UIUtil from '../util/UIUtil';
const 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 the height of filmstrip
* @returns {number} height * @returns {number} height
@ -36,8 +11,8 @@ const Filmstrip = {
// FIXME Make it more clear the getFilmstripHeight check is used in // FIXME Make it more clear the getFilmstripHeight check is used in
// horizontal film strip mode for calculating how tall large video // horizontal film strip mode for calculating how tall large video
// display should be. // display should be.
if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) { if (isFilmstripVisible(APP.store) && !interfaceConfig.VERTICAL_FILMSTRIP) {
return $(`.${this.filmstripContainerClassName}`).outerHeight(); return $('.filmstrip').outerHeight();
} }
return 0; return 0;
@ -48,311 +23,143 @@ const Filmstrip = {
* @returns {number} width * @returns {number} width
*/ */
getFilmstripWidth() { getFilmstripWidth() {
return this.isFilmstripVisible() const filmstrip = $('#remoteVideos');
? this.filmstrip.outerWidth()
- parseInt(this.filmstrip.css('paddingLeft'), 10) return isFilmstripVisible(APP.store)
- parseInt(this.filmstrip.css('paddingRight'), 10) ? filmstrip.outerWidth()
- parseInt(filmstrip.css('paddingLeft'), 10)
- parseInt(filmstrip.css('paddingRight'), 10)
: 0; : 0;
}, },
/** /**
* Calculates the size for thumbnails: local and remote one * Resizes thumbnails for tile view.
* @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.
* *
* @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() { resizeThumbnailsForTileView(width, height, forceUpdate = false) {
/** const thumbs = this._getThumbs(!forceUpdate);
* If the videoAreaAvailableWidth is set we use this one to calculate const avatarSize = height / 2;
* 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);
if (thumbs.localThumb) { if (thumbs.localThumb) {
// eslint-disable-next-line no-shadow
thumbs.localThumb.css({ thumbs.localThumb.css({
display: 'inline-block', 'padding-top': '',
height: `${local.thumbHeight}px`, height: `${height}px`,
'min-height': `${local.thumbHeight}px`, 'min-height': `${height}px`,
'min-width': `${local.thumbWidth}px`, 'min-width': `${width}px`,
width: `${local.thumbWidth}px` width: `${width}px`
}); });
const avatarSize = local.thumbHeight / 2;
thumbs.localThumb.find('.avatar-container')
.height(avatarSize)
.width(avatarSize);
} }
if (thumbs.remoteThumbs) { if (thumbs.remoteThumbs) {
thumbs.remoteThumbs.css({ thumbs.remoteThumbs.css({
display: 'inline-block', 'padding-top': '',
height: `${remote.thumbHeight}px`, height: `${height}px`,
'min-height': `${remote.thumbHeight}px`, 'min-height': `${height}px`,
'min-width': `${remote.thumbWidth}px`, 'min-width': `${width}px`,
width: `${remote.thumbWidth}px` width: `${width}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`
}); });
} }
const { localThumb } = this.getThumbs(); $('.avatar-container').css({
const height = localThumb ? localThumb.height() : 0; height: `${avatarSize}px`,
const fontSize = UIUtil.getIndicatorFontSize(height); width: `${avatarSize}px`
this.filmstrip.find('.indicator').css({
'font-size': `${fontSize}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 * Returns thumbnails of the filmstrip
* @param onlyVisible * @param onlyVisible
* @returns {object} thumbnails * @returns {object} thumbnails
*/ */
getThumbs(onlyVisible = false) { _getThumbs(onlyVisible = false) {
let selector = 'span'; let selector = 'span';
if (onlyVisible) { if (onlyVisible) {
@ -360,7 +167,7 @@ const Filmstrip = {
} }
const localThumb = $('#localVideoContainer'); const localThumb = $('#localVideoContainer');
const remoteThumbs = this.filmstripRemoteVideos.children(selector); const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
// Exclude the local video container if it has been hidden. // Exclude the local video container if it has been hidden.
if (localThumb.hasClass('hidden')) { if (localThumb.hasClass('hidden')) {

View File

@ -490,9 +490,15 @@ export default class LargeVideoManager {
show = APP.conference.isConnectionInterrupted(); 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) { if (show) {
// Avatar message conflicts with 'videoConnectionMessage', // Avatar message conflicts with 'videoConnectionMessage',

View File

@ -33,6 +33,8 @@ export default class LocalVideo extends SmallVideo {
this.streamEndedCallback = streamEndedCallback; this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer(); this.container = this.createContainer();
this.$container = $(this.container); this.$container = $(this.container);
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation(); this.updateDOMLocation();
this.localVideoId = null; this.localVideoId = null;
@ -40,10 +42,8 @@ export default class LocalVideo extends SmallVideo {
if (!config.disableLocalVideoFlip) { if (!config.disableLocalVideoFlip) {
this._buildContextMenu(); this._buildContextMenu();
} }
this.isLocal = true;
this.emitter = emitter; this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
? 'left top' : 'top center';
Object.defineProperty(this, 'id', { Object.defineProperty(this, 'id', {
get() { get() {

View File

@ -20,10 +20,7 @@ import {
REMOTE_CONTROL_MENU_STATES, REMOTE_CONTROL_MENU_STATES,
RemoteVideoMenuTriggerButton RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu'; } from '../../../react/features/remote-video-menu';
import { import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
LAYOUTS,
getCurrentLayout
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -129,6 +126,7 @@ export default class RemoteVideo extends SmallVideo {
addRemoteVideoContainer() { addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId); this.container = createContainer(this.videoSpanId);
this.$container = $(this.container); this.$container = $(this.container);
this._setThumbnailSize();
this.initBrowserSpecificProperties(); this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu(); this.updateRemoteVideoMenu();
this.addAudioLevelIndicator(); this.addAudioLevelIndicator();

View File

@ -34,7 +34,6 @@ import {
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents'; import UIEvents from '../../../service/UI/UIEvents';
/** /**
@ -629,8 +628,7 @@ export default class SmallVideo {
<Provider store = { APP.store }> <Provider store = { APP.store }>
<AvatarDisplay <AvatarDisplay
className = 'userAvatar' className = 'userAvatar'
participantId = { this.id } participantId = { this.id } />
size = { this.$avatar().width() } />
</Provider>, </Provider>,
thumbnail thumbnail
); );
@ -801,7 +799,8 @@ export default class SmallVideo {
return; 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 showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState(); const state = APP.store.getState();
const currentLayout = getCurrentLayout(state); const currentLayout = getCurrentLayout(state);
@ -830,11 +829,9 @@ export default class SmallVideo {
connectionStatus = { this._connectionStatus } connectionStatus = { this._connectionStatus }
iconSize = { iconSize } iconSize = { iconSize }
isLocalVideo = { this.isLocal } isLocalVideo = { this.isLocal }
enableStatsDisplay enableStatsDisplay = { !interfaceConfig.filmStripOnly }
= { !interfaceConfig.filmStripOnly }
participantId = { this.id } participantId = { this.id }
statsPopoverPosition statsPopoverPosition = { statsPopoverPosition } />
= { statsPopoverPosition } />
: null } : null }
<RaisedHandIndicator <RaisedHandIndicator
iconSize = { iconSize } iconSize = { iconSize }
@ -936,4 +933,67 @@ export default class SmallVideo {
this._popoverIsHovered = popoverIsHovered; this._popoverIsHovered = popoverIsHovered;
this.updateView(); 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 */ /* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
import {
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality
} from '../../../react/features/base/conference';
import { import {
JitsiParticipantConnectionStatus JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet'; } from '../../../react/features/base/lib-jitsi-meet';
@ -14,13 +10,9 @@ import {
getPinnedParticipant, getPinnedParticipant,
pinParticipant pinParticipant
} from '../../../react/features/base/participants'; } from '../../../react/features/base/participants';
import {
shouldDisplayTileView
} from '../../../react/features/video-layout';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo'; import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb'; import SharedVideoThumb from '../shared_video/SharedVideoThumb';
import Filmstrip from './Filmstrip';
import UIEvents from '../../../service/UI/UIEvents'; import UIEvents from '../../../service/UI/UIEvents';
import RemoteVideo from './RemoteVideo'; import RemoteVideo from './RemoteVideo';
@ -87,11 +79,6 @@ const VideoLayout = {
// sets default video type of local video // sets default video type of local video
// FIXME container type is totally different thing from the video type // FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_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(); this.registerListeners();
}, },
@ -331,8 +318,6 @@ const VideoLayout = {
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE); remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
} }
VideoLayout.resizeThumbnails(true);
// Initialize the view // Initialize the view
remoteVideo.updateView(); remoteVideo.updateView();
}, },
@ -340,12 +325,9 @@ const VideoLayout = {
// FIXME: what does this do??? // FIXME: what does this do???
remoteVideoActive(videoElement, resourceJid) { remoteVideoActive(videoElement, resourceJid) {
logger.info(`${resourceJid} video is now active`, videoElement); logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails( if (videoElement) {
false, () => { $(videoElement).show();
if (videoElement) { }
$(videoElement).show();
}
});
this._updateLargeVideoIfDisplayed(resourceJid, true); 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. * 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 * Hides all the indicators
*/ */
@ -586,8 +525,6 @@ const VideoLayout = {
} else { } else {
logger.warn(`No remote video for ${id}`); logger.warn(`No remote video for ${id}`);
} }
VideoLayout.resizeThumbnails();
}, },
onVideoTypeChanged(id, newVideoType) { onVideoTypeChanged(id, newVideoType) {
@ -623,12 +560,7 @@ const VideoLayout = {
* *
* @param forceUpdate indicates that hidden thumbnails will be shown * @param forceUpdate indicates that hidden thumbnails will be shown
*/ */
resizeVideoArea( resizeVideoArea(animate = false) {
forceUpdate = false,
animate = false) {
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
if (largeVideo) { if (largeVideo) {
largeVideo.updateContainerSize(); largeVideo.updateContainerSize();
largeVideo.resize(animate); largeVideo.resize(animate);
@ -917,6 +849,10 @@ const VideoLayout = {
refreshLayout() { refreshLayout() {
localVideoThumbnail && localVideoThumbnail.updateDOMLocation(); localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
VideoLayout.resizeVideoArea(); 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)) { if (this.isCurrentlyOnLarge(participantId)) {
this.updateLargeVideo(participantId, force); this.updateLargeVideo(participantId, force);
} }
},
/**
* Handles window resizes.
*/
onResize() {
VideoLayout.resizeVideoArea();
} }
}; };

View File

@ -116,16 +116,12 @@ StateListenerRegistry.register(
maxReceiverVideoQuality, maxReceiverVideoQuality,
preferredReceiverVideoQuality preferredReceiverVideoQuality
} = currentState; } = currentState;
const changedPreferredVideoQuality = preferredReceiverVideoQuality const changedPreferredVideoQuality
!== previousState.preferredReceiverVideoQuality; = preferredReceiverVideoQuality !== previousState.preferredReceiverVideoQuality;
const changedMaxVideoQuality = maxReceiverVideoQuality const changedMaxVideoQuality = maxReceiverVideoQuality !== previousState.maxReceiverVideoQuality;
!== previousState.maxReceiverVideoQuality;
if (changedPreferredVideoQuality || changedMaxVideoQuality) { if (changedPreferredVideoQuality || changedMaxVideoQuality) {
_setReceiverVideoConstraint( _setReceiverVideoConstraint(conference, preferredReceiverVideoQuality, maxReceiverVideoQuality);
conference,
preferredReceiverVideoQuality,
maxReceiverVideoQuality);
} }
}); });

View File

@ -72,7 +72,6 @@ class BaseIndicator extends Component<Props> {
*/ */
static defaultProps = { static defaultProps = {
className: '', className: '',
iconSize: 13,
id: '', id: '',
tooltipPosition: 'top' tooltipPosition: 'top'
}; };
@ -95,8 +94,12 @@ class BaseIndicator extends Component<Props> {
tooltipKey, tooltipKey,
tooltipPosition tooltipPosition
} = this.props; } = this.props;
const iconContainerClassName = `indicator-icon-container ${className}`; const iconContainerClassName = `indicator-icon-container ${className}`;
const style = {};
if (iconSize) {
style.fontSize = iconSize;
}
return ( return (
<div className = 'indicator-container'> <div className = 'indicator-container'>
@ -110,7 +113,7 @@ class BaseIndicator extends Component<Props> {
className = { iconClassName } className = { iconClassName }
id = { iconId } id = { iconId }
src = { icon } src = { icon }
style = {{ fontSize: iconSize }} /> style = { style } />
</span> </span>
</Tooltip> </Tooltip>
</div> </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 * The type of (redux) action which sets the aspect ratio of the app's user
* interface. * interface.

View File

@ -1,10 +1,10 @@
// @flow // @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 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. * 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; 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 * Sets the aspect ratio of the app's user interface based on specific width and
* height. * 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 { 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'; import { ASPECT_RATIO_NARROW } from './constants';
const {
innerHeight = 0,
innerWidth = 0
} = window;
/** /**
* The default/initial redux state of the feature base/responsive-ui. * The default/initial redux state of the feature base/responsive-ui.
*/ */
const DEFAULT_STATE = { const DEFAULT_STATE = {
aspectRatio: ASPECT_RATIO_NARROW, aspectRatio: ASPECT_RATIO_NARROW,
clientHeight: innerHeight,
clientWidth: innerWidth,
reducedUI: false reducedUI: false
}; };
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => { ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case CLIENT_RESIZED: {
return {
...state,
clientWidth: action.clientWidth,
clientHeight: action.clientHeight
};
}
case SET_ASPECT_RATIO: case SET_ASPECT_RATIO:
return set(state, 'aspectRatio', action.aspectRatio); 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'; 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' className = 'audioMuted toolbar-icon'
icon = { IconMicDisabled } icon = { IconMicDisabled }
iconId = 'mic-disabled' iconId = 'mic-disabled'
iconSize = { 13 }
tooltipKey = 'videothumbnail.mute' tooltipKey = 'videothumbnail.mute'
tooltipPosition = { this.props.tooltipPosition } /> tooltipPosition = { this.props.tooltipPosition } />
); );

View File

@ -16,6 +16,8 @@ import { dockToolbox } from '../../../toolbox';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions'; import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions'; import { shouldRemoteVideosBeVisible } from '../../functions';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import Toolbar from './Toolbar'; import Toolbar from './Toolbar';
declare var APP: Object; declare var APP: Object;
@ -31,17 +33,37 @@ type Props = {
*/ */
_className: string, _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. * Whether the UI/UX is filmstrip-only.
*/ */
_filmstripOnly: boolean, _filmstripOnly: boolean,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/** /**
* Whether or not remote videos are currently being hovered over. Hover * Whether or not remote videos are currently being hovered over. Hover
* handling is currently being handled detected outside of react. * handling is currently being handled detected outside of react.
*/ */
_hovered: boolean, _hovered: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/** /**
* Additional CSS class names to add to the container of all the thumbnails. * 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 // also works around an issue where mouseout and then a mouseover event
// is fired when hovering over remote thumbnails, which are not yet in // is fired when hovering over remote thumbnails, which are not yet in
// react. // react.
this._notifyOfHoveredStateUpdate this._notifyOfHoveredStateUpdate = _.debounce(this._notifyOfHoveredStateUpdate, 100);
= _.debounce(this._notifyOfHoveredStateUpdate, 100);
// Cache the current hovered state for _updateHoveredState to always // Cache the current hovered state for _updateHoveredState to always
// send the last known hovered state. // 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. // Bind event handlers so they are only bound once for every instance.
this._onMouseOut = this._onMouseOut.bind(this); this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this); this._onMouseOver = this._onMouseOver.bind(this);
this._onShortcutToggleFilmstrip this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
= this._onShortcutToggleFilmstrip.bind(this); this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.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 // will get updated without replacing the DOM. If the known DOM gets
// modified, then the views will get blown away. // 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 ( return (
<div className = { `filmstrip ${this.props._className}` }> <div className = { `filmstrip ${this.props._className}` }>
{ this.props._filmstripOnly { this.props._filmstripOnly
? <Toolbar /> : this._renderToggleButton() } ? <Toolbar /> : this._renderToggleButton() }
<div <div
className = { this.props._videosClassName } className = { this.props._videosClassName }
id = 'remoteVideos'> id = 'remoteVideos'
style = { remoteVideosStyle }>
<div <div
className = 'filmstrip__videos' className = 'filmstrip__videos'
id = 'filmstripLocalVideo' id = 'filmstripLocalVideo'
@ -165,10 +207,11 @@ class Filmstrip extends Component <Props> {
* thumbnails resize instead of causing overflow. * thumbnails resize instead of causing overflow.
*/} */}
<div <div
className = 'remote-videos-container' className = { remoteVideoContainerClassName }
id = 'filmstripRemoteVideosContainer' id = 'filmstripRemoteVideosContainer'
onMouseOut = { this._onMouseOut } onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }> onMouseOver = { this._onMouseOver }
style = { filmstripRemoteVideosContainerStyle }>
<div id = 'localVideoTileViewContainer' /> <div id = 'localVideoTileViewContainer' />
</div> </div>
</div> </div>
@ -301,20 +344,24 @@ class Filmstrip extends Component <Props> {
function _mapStateToProps(state) { function _mapStateToProps(state) {
const { hovered, visible } = state['features/filmstrip']; const { hovered, visible } = state['features/filmstrip'];
const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly); const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly);
const reduceHeight = !isFilmstripOnly const reduceHeight
&& state['features/toolbox'].visible = !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
&& interfaceConfig.TOOLBAR_BUTTONS.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state); const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${ const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`.trim();
reduceHeight ? 'reduce-height' : ''}`.trim();
const videosClassName = `filmstrip__videos${ const videosClassName = `filmstrip__videos${
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${ isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
visible ? '' : ' hidden'}`; visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
return { return {
_className: className, _className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripOnly: isFilmstripOnly, _filmstripOnly: isFilmstripOnly,
_filmstripWidth: filmstripWidth,
_hovered: hovered, _hovered: hovered,
_rows: gridDimensions.rows,
_videosClassName: videosClassName, _videosClassName: videosClassName,
_visible: visible _visible: visible
}; };

View File

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

View File

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

View File

@ -4,3 +4,8 @@
* The height of the filmstrip in narrow aspect ratio, or width in wide. * The height of the filmstrip in narrow aspect ratio, or width in wide.
*/ */
export const FILMSTRIP_SIZE = 90; 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'; } from '../base/participants';
import { toState } from '../base/redux'; import { toState } from '../base/redux';
import { TILE_ASPECT_RATIO } from './constants';
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
/** /**
@ -59,3 +61,58 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| state['features/base/config'].disable1On1Mode); || 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'; export * from './functions';
import './reducer'; 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 { import {
SET_FILMSTRIP_ENABLED, SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_HOVERED, SET_FILMSTRIP_HOVERED,
SET_FILMSTRIP_VISIBLE SET_FILMSTRIP_VISIBLE,
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS
} from './actionTypes'; } from './actionTypes';
const DEFAULT_STATE = { const DEFAULT_STATE = {
@ -17,6 +19,22 @@ const DEFAULT_STATE = {
*/ */
enabled: true, 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. * The indicator which determines whether the {@link Filmstrip} is visible.
* *
@ -55,6 +73,17 @@ ReducerRegistry.register(
...state, ...state,
visible: action.visible 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; 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 * equal count of tiles for height and width, until maxColumn is reached in
* which rows will be added but no more columns. * 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 * @param {number} maxColumns - The maximum number of columns that can be
* displayed. * displayed.
* @returns {Object} An object is return with the desired number of columns, * @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. * rows, and visible rows (the rest should overflow) for the tile view layout.
*/ */
export function getTileViewGridDimensions(state: Object, maxColumns: number) { export function getTileViewGridDimensions(numberOfParticipants: number, maxColumns: number = getMaxColumnCount()) {
// Purposefully include all participants, which includes fake participants const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
// that should show a thumbnail.
const potentialThumbnails = state['features/base/participants'].length;
const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails));
const columns = Math.min(columnsToMaintainASquare, maxColumns); const columns = Math.min(columnsToMaintainASquare, maxColumns);
const rows = Math.ceil(potentialThumbnails / columns); const rows = Math.ceil(numberOfParticipants / columns);
const visibleRows = Math.min(maxColumns, rows); const visibleRows = Math.min(maxColumns, rows);
return { return {
columns, columns,
rows,
visibleRows visibleRows
}; };
} }