diff --git a/css/_utils.scss b/css/_utils.scss index ca14db617e..629000c56a 100644 --- a/css/_utils.scss +++ b/css/_utils.scss @@ -1,3 +1,11 @@ +.flip-x { + transform: scaleX(-1); +} + +.hidden { + display: none; +} + /** * Hides an element. */ @@ -5,6 +13,10 @@ display: none !important; } +.invisible { + visibility: hidden; +} + /** * Shows an element. */ @@ -36,7 +48,3 @@ display: -webkit-flex !important; display: flex !important; } - -.hidden { - display: none; -} diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 6595286a8d..169f0a89ee 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -12,7 +12,8 @@ overflow: hidden; } -.video_blurred_container { +#largeVideoBackgroundContainer, +.large-video-background { height: 100%; filter: blur(40px); left: 0; @@ -20,6 +21,16 @@ position: absolute; top: 0; width: 100%; + + &.fit-full-height #largeVideoBackground { + height: 100%; + width: auto; + } + + .fit-full-width #largeVideoBackground { + height: auto; + width: 100%; + } } .videocontainer { diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index d7d6565b82..1ec9578f16 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -1,5 +1,16 @@ /* global $, APP, interfaceConfig */ +/* eslint-disable no-unused-vars */ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { browser } from '../../../react/features/base/lib-jitsi-meet'; +import { + ORIENTATION, + LargeVideoBackground +} from '../../../react/features/large-video'; +/* eslint-enable no-unused-vars */ + import Filmstrip from './Filmstrip'; import LargeContainer from './LargeContainer'; import UIEvents from '../../../service/UI/UIEvents'; @@ -10,7 +21,23 @@ export const VIDEO_CONTAINER_TYPE = 'camera'; const FADE_DURATION_MS = 300; -const logger = require('jitsi-meet-logger').getLogger(__filename); +/** + * The CSS class used to add a filter effect on the large video when there is + * a problem with local video. + * + * @private + * @type {string} + */ +const LOCAL_PROBLEM_FILTER_CLASS = 'videoProblemFilter'; + +/** + * The CSS class used to add a filter effect on the large video when there is + * a problem with remote video. + * + * @private + * @type {string} + */ +const REMOTE_PROBLEM_FILTER_CLASS = 'remoteVideoProblemFilter'; /** * Returns an array of the video dimensions, so that it keeps it's aspect @@ -167,13 +194,6 @@ export class VideoContainer extends LargeContainer { return $('#largeVideo'); } - /** - * - */ - get $videoBackground() { - return $('#largeVideoBackground'); - } - /** * */ @@ -199,6 +219,23 @@ export class VideoContainer extends LargeContainer { this.isVisible = false; + /** + * Whether the background should fit the height of the container + * (portrait) or fit the width of the container (landscape). + * + * @private + * @type {string|null} + */ + this._backgroundOrientation = null; + + /** + * Flag indicates whether or not the background should be rendered. + * If the background will not be visible then it is hidden to save + * on performance. + * @type {boolean} + */ + this._hideBackground = true; + /** * Flag indicates whether or not the avatar is currently displayed. * @type {boolean} @@ -277,8 +314,8 @@ export class VideoContainer extends LargeContainer { * false otherwise. */ enableLocalConnectionProblemFilter(enable) { - this.$video.toggleClass('videoProblemFilter', enable); - this.$videoBackground.toggleClass('videoProblemFilter', enable); + this.$video.toggleClass(LOCAL_PROBLEM_FILTER_CLASS, enable); + this._updateBackground(); } /** @@ -402,23 +439,19 @@ export class VideoContainer extends LargeContainer { return; } - this._hideVideoBackground(); - const [ width, height ] = this.getVideoSize(containerWidth, containerHeight); if ((containerWidth > width) || (containerHeight > height)) { - this._showVideoBackground(); - const css - = containerWidth > width - ? { width: '100%', - height: 'auto' } - : { width: 'auto', - height: '100%' }; - - this.$videoBackground.css(css); + this._backgroundOrientation = containerWidth > width + ? ORIENTATION.LANDSCAPE : ORIENTATION.PORTRAIT; + this._hideBackground = false; + } else { + this._hideBackground = true; } + this._updateBackground(); + const { horizontalIndent, verticalIndent } = this.getVideoPosition(width, height, containerWidth, containerHeight); @@ -484,7 +517,6 @@ export class VideoContainer extends LargeContainer { // detach old stream if (this.stream) { this.stream.detach(this.$video[0]); - this.stream.detach(this.$videoBackground[0]); } this.stream = stream; @@ -495,18 +527,14 @@ export class VideoContainer extends LargeContainer { } stream.attach(this.$video[0]); - stream.attach(this.$videoBackground[0]); - - this._hideVideoBackground(); const flipX = stream.isLocal() && this.localFlipX; this.$video.css({ transform: flipX ? 'scaleX(-1)' : 'none' }); - this.$videoBackground.css({ - transform: flipX ? 'scaleX(-1)' : 'none' - }); + + this._updateBackground(); // Reset the large video background depending on the stream. this.setLargeVideoBackground(this.avatarDisplayed); @@ -525,9 +553,7 @@ export class VideoContainer extends LargeContainer { transform: this.localFlipX ? 'scaleX(-1)' : 'none' }); - this.$videoBackground.css({ - transform: this.localFlipX ? 'scaleX(-1)' : 'none' - }); + this._updateBackground(); } @@ -567,10 +593,9 @@ export class VideoContainer extends LargeContainer { * the indication. */ showRemoteConnectionProblemIndicator(show) { - this.$video.toggleClass('remoteVideoProblemFilter', show); - this.$videoBackground.toggleClass('remoteVideoProblemFilter', show); - - this.$avatar.toggleClass('remoteVideoProblemFilter', show); + this.$video.toggleClass(REMOTE_PROBLEM_FILTER_CLASS, show); + this.$avatar.toggleClass(REMOTE_PROBLEM_FILTER_CLASS, show); + this._updateBackground(); } @@ -643,17 +668,6 @@ export class VideoContainer extends LargeContainer { ? '#000' : interfaceConfig.DEFAULT_BACKGROUND); } - /** - * Sets the blur background to be invisible and pauses any playing video. - * - * @private - * @returns {void} - */ - _hideVideoBackground() { - this.$videoBackground.css({ visibility: 'hidden' }); - this.$videoBackground[0].pause(); - } - /** * Callback invoked when the video element changes dimensions. * @@ -665,26 +679,33 @@ export class VideoContainer extends LargeContainer { } /** - * Sets the blur background to be visible and starts any loaded video. + * Attaches and/or updates a React Component to be used as a background for + * the large video, to display blurred video and fill up empty space not + * taken up by the large video. * * @private * @returns {void} */ - _showVideoBackground() { - this.$videoBackground.css({ visibility: 'visible' }); - - // XXX HTMLMediaElement.play's Promise may be rejected. Certain - // environments such as Google Chrome and React Native will report the - // rejection as unhandled. And that may appear scary depending on how - // the environment words the report. To reduce the risk of scaring a - // developer, make sure that the rejection is handled. We cannot really - // do anything substantial about the rejection and, more importantly, we - // do not care. Some browsers (at this time, only Edge is known) don't - // return a promise from .play(), so check before trying to catch. - const res = this.$videoBackground[0].play(); - - if (typeof res !== 'undefined') { - res.catch(reason => logger.error(reason)); + _updateBackground() { + if (browser.isFirefox() || browser.isTemasysPluginUsed()) { + return; } + + ReactDOM.render( +