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:
parent
ca9ca04d0f
commit
31d9fb12c8
|
@ -31,13 +31,6 @@
|
|||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows as a list item
|
||||
**/
|
||||
.show-list-item {
|
||||
display: list-item !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a flex element.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.filmstrip__videos .videocontainer {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
border: $thumbnailVideoBorder solid transparent;
|
||||
|
|
|
@ -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 **/
|
||||
|
|
|
@ -56,6 +56,10 @@
|
|||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.indicator-icon-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.indicator-container {
|
||||
float: none;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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 } />
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,3 +5,5 @@ export * from './constants';
|
|||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
import './subscriber';
|
||||
import './middleware';
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue