Merge pull request #7651 from vitormateusalmeida/issue-7598
Draggable webcams grid improvements
This commit is contained in:
commit
34b727542a
@ -52,6 +52,7 @@ export default class Media extends Component {
|
||||
disableVideo,
|
||||
children,
|
||||
audioModalIsOpen,
|
||||
usersVideo,
|
||||
} = this.props;
|
||||
|
||||
const contentClassName = cx({
|
||||
@ -70,7 +71,12 @@ export default class Media extends Component {
|
||||
className={cx(styles.container)}
|
||||
ref={this.refContainer}
|
||||
>
|
||||
<div className={!swapLayout ? contentClassName : overlayClassName}>
|
||||
<div
|
||||
className={!swapLayout ? contentClassName : overlayClassName}
|
||||
style={{
|
||||
maxHeight: usersVideo.length < 1 || floatingOverlay ? '100%' : '80%',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<WebcamDraggableOverlay
|
||||
@ -80,6 +86,7 @@ export default class Media extends Component {
|
||||
hideOverlay={hideOverlay}
|
||||
disableVideo={disableVideo}
|
||||
audioModalIsOpen={audioModalIsOpen}
|
||||
usersVideo={usersVideo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -123,6 +123,7 @@ export default withModalMounter(withTracker(() => {
|
||||
}
|
||||
|
||||
const usersVideo = VideoService.getAllUsersVideo();
|
||||
data.usersVideo = usersVideo;
|
||||
if (MediaService.shouldShowOverlay() && usersVideo.length && viewParticipantsWebcams) {
|
||||
data.floatingOverlay = usersVideo.length < 2;
|
||||
data.hideOverlay = usersVideo.length === 0;
|
||||
|
@ -24,27 +24,28 @@
|
||||
|
||||
.overlay {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
z-index: 2;
|
||||
align-items: center;
|
||||
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
max-height: var(--video-height);
|
||||
min-height: var(--video-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlayRelative{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.overlayAbsoluteSingle{
|
||||
position: absolute;
|
||||
height: calc(var(--video-width) / var(--video-ratio));
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.overlayAbsoluteMult{
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.overlayToTop {
|
||||
@ -65,18 +66,19 @@
|
||||
}
|
||||
|
||||
.floatingOverlay {
|
||||
margin: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@include mq($medium-up) {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: var(--video-width);
|
||||
min-width: var(--video-width);
|
||||
max-width: var(--video-max-width);
|
||||
height: calc(var(--video-width) / var(--video-ratio));
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
width: fit-content;
|
||||
min-width: calc(var(--video-height) * var(--video-ratio));
|
||||
max-width: fit-content;
|
||||
height: fit-content;
|
||||
min-height: var(--video-height);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,4 +123,4 @@
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import browser from 'browser-detect';
|
||||
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import { styles } from '../styles';
|
||||
import { styles } from '../styles.scss';
|
||||
|
||||
const propTypes = {
|
||||
floatingOverlay: PropTypes.bool,
|
||||
@ -29,49 +29,18 @@ const fullscreenChangedEvents = [
|
||||
const BROWSER_ISMOBILE = browser().mobile;
|
||||
|
||||
export default class WebcamDraggableOverlay extends Component {
|
||||
static getWebcamBySelector() {
|
||||
return document.querySelector('video[class^="media"]');
|
||||
static getWebcamGridBySelector() {
|
||||
return document.querySelector('div[class*="videoList"]');
|
||||
}
|
||||
|
||||
static getWebcamBySelectorCount() {
|
||||
return document.querySelectorAll('video[class^="media"]').length;
|
||||
}
|
||||
|
||||
static getWebcamListBySelector() {
|
||||
return document.querySelector('div[class^="videoList"]');
|
||||
}
|
||||
|
||||
static getVideoCanvasBySelector() {
|
||||
return document.querySelector('div[class^="videoCanvas"]');
|
||||
static getVideoCountBySelector() {
|
||||
return document.querySelectorAll('video[class*="media"]').length;
|
||||
}
|
||||
|
||||
static getOverlayBySelector() {
|
||||
return document.querySelector('div[class*="overlay"]');
|
||||
}
|
||||
|
||||
static isOverlayAbsolute() {
|
||||
return !!(document.querySelector('div[class*="overlayAbsolute"]'));
|
||||
}
|
||||
|
||||
static getIsOverlayChanged() {
|
||||
const overlayToTop = document.querySelector('div[class*="overlayToTop"]');
|
||||
const overlayToBottom = document.querySelector('div[class*="overlayToBottom"]');
|
||||
|
||||
return !!(overlayToTop || overlayToBottom);
|
||||
}
|
||||
|
||||
static getGridLineNum(numCams, camWidth, containerWidth) {
|
||||
let used = (camWidth + 10) * numCams;
|
||||
let countLines = 0;
|
||||
|
||||
while (used > containerWidth) {
|
||||
used -= containerWidth;
|
||||
countLines += 1;
|
||||
}
|
||||
|
||||
return countLines + 1;
|
||||
}
|
||||
|
||||
static waitFor(condition, callback) {
|
||||
const cond = condition();
|
||||
if (!cond) {
|
||||
@ -90,19 +59,28 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
showDropZones: false,
|
||||
showBgDropZoneTop: false,
|
||||
showBgDropZoneBottom: false,
|
||||
dropOnTop: BROWSER_ISMOBILE,
|
||||
dropOnBottom: !BROWSER_ISMOBILE,
|
||||
initialPosition: { x: 0, y: 0 },
|
||||
initialRectPosition: { x: 0, y: 0 },
|
||||
lastPosition: { x: 0, y: 0 },
|
||||
dropOnTop: true,
|
||||
dropOnBottom: false,
|
||||
initialPosition: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
initialRectPosition: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
lastPosition: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
resetPosition: false,
|
||||
isFullScreen: false,
|
||||
isVideoLoaded: false,
|
||||
isMinWidth: false,
|
||||
userLength: 0,
|
||||
shouldUpdatePosition: true,
|
||||
};
|
||||
|
||||
this.shouldUpdatePosition = true;
|
||||
|
||||
this.updateWebcamPositionByResize = this.updateWebcamPositionByResize.bind(this);
|
||||
this.eventVideoFocusChangeListener = this.eventVideoFocusChangeListener.bind(this);
|
||||
|
||||
@ -122,15 +100,13 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
this.handleFullscreenChange = this.handleFullscreenChange.bind(this);
|
||||
this.fullscreenButtonChange = this.fullscreenButtonChange.bind(this);
|
||||
|
||||
this.getVideoListUsersChange = this.getVideoListUsersChange.bind(this);
|
||||
this.setIsFullScreen = this.setIsFullScreen.bind(this);
|
||||
this.setResetPosition = this.setResetPosition.bind(this);
|
||||
this.setInitialReferencePoint = this.setInitialReferencePoint.bind(this);
|
||||
this.setLastPosition = this.setLastPosition.bind(this);
|
||||
this.setShouldUpdatePosition = this.setShouldUpdatePosition.bind(this);
|
||||
this.setLastWebcamPosition = this.setLastWebcamPosition.bind(this);
|
||||
this.setisMinWidth = this.setisMinWidth.bind(this);
|
||||
this.setDropOnBottom = this.setDropOnBottom.bind(this);
|
||||
this.setDropOnTop = this.setDropOnTop.bind(this);
|
||||
|
||||
this.dropZoneTopEnterHandler = this.dropZoneTopEnterHandler.bind(this);
|
||||
this.dropZoneTopLeaveHandler = this.dropZoneTopLeaveHandler.bind(this);
|
||||
@ -147,7 +123,9 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
const { resetPosition } = this.state;
|
||||
|
||||
if (!floatingOverlay
|
||||
&& !resetPosition) this.setResetPosition(true);
|
||||
&& !resetPosition) {
|
||||
this.setResetPosition(true);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.eventResizeListener);
|
||||
window.addEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
|
||||
@ -158,41 +136,61 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
|
||||
// Ensures that the event will be called before the resize
|
||||
document.addEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
|
||||
|
||||
window.addEventListener('videoListUsersChange', this.getVideoListUsersChange);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { swapLayout } = this.props;
|
||||
const { userLength, lastPosition } = this.state;
|
||||
componentDidUpdate(prevProps) {
|
||||
const { swapLayout, usersVideo, mediaContainer } = this.props;
|
||||
const { lastPosition } = this.state;
|
||||
const { y } = lastPosition;
|
||||
// if (prevProps.swapLayout && !swapLayout && userLength === 1) {
|
||||
// this.setShouldUpdatePosition(false);
|
||||
// }
|
||||
const userLength = usersVideo.length;
|
||||
const prevUserLength = prevProps.usersVideo.length;
|
||||
|
||||
if (prevProps.mediaContainer && mediaContainer) {
|
||||
const mediaContainerRect = mediaContainer.getBoundingClientRect();
|
||||
const {
|
||||
left: mediaLeft,
|
||||
top: mediaTop,
|
||||
} = mediaContainerRect;
|
||||
const prevMediaContainerRect = prevProps.mediaContainer.getBoundingClientRect();
|
||||
const {
|
||||
left: prevMediaLeft,
|
||||
top: prevMediaTop,
|
||||
} = prevMediaContainerRect;
|
||||
|
||||
if (mediaLeft !== prevMediaLeft || mediaTop !== prevMediaTop) {
|
||||
this.shouldUpdatePosition = false;
|
||||
} else if (this.shouldUpdatePosition === false) {
|
||||
this.shouldUpdatePosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.swapLayout && !swapLayout && userLength === 1) {
|
||||
this.shouldUpdatePosition = false;
|
||||
}
|
||||
if (prevProps.swapLayout && !swapLayout && userLength > 1) {
|
||||
this.setLastPosition(0, y);
|
||||
}
|
||||
if (prevState.userLength === 1 && userLength > 1) {
|
||||
this.setDropOnBottom(true);
|
||||
if (prevUserLength === 1 && userLength > 1) {
|
||||
this.setResetPosition(true);
|
||||
this.setDropOnTop(true);
|
||||
}
|
||||
if (prevUserLength !== userLength) {
|
||||
WebcamDraggableOverlay.waitFor(
|
||||
() => WebcamDraggableOverlay.getVideoCountBySelector() === userLength,
|
||||
this.updateWebcamPositionByResize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
fullscreenChangedEvents.forEach((event) => {
|
||||
document.removeEventListener(event, this.fullScreenToggleCallback);
|
||||
document.removeEventListener(event, this.handleFullscreenChange);
|
||||
});
|
||||
|
||||
document.removeEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange);
|
||||
document.removeEventListener('videoListUsersChange', this.getVideoListUsersChange);
|
||||
document.removeEventListener('videoFocusChange', this.eventVideoFocusChangeListener);
|
||||
}
|
||||
|
||||
getVideoListUsersChange() {
|
||||
const userLength = WebcamDraggableOverlay.getWebcamBySelectorCount();
|
||||
this.setState({ userLength });
|
||||
}
|
||||
|
||||
setIsFullScreen(isFullScreen) {
|
||||
this.setState({ isFullScreen });
|
||||
}
|
||||
@ -202,30 +200,26 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
}
|
||||
|
||||
setLastPosition(x, y) {
|
||||
this.setState({ lastPosition: { x, y } });
|
||||
this.setState({
|
||||
lastPosition: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setShouldUpdatePosition(shouldUpdatePosition) {
|
||||
this.setState({ shouldUpdatePosition });
|
||||
}
|
||||
|
||||
setDropOnBottom(dropOnBottom) {
|
||||
this.setState({ dropOnBottom });
|
||||
setDropOnTop(dropOnTop) {
|
||||
this.setState({ dropOnTop });
|
||||
}
|
||||
|
||||
setInitialReferencePoint() {
|
||||
const { refMediaContainer } = this.props;
|
||||
const { userLength, shouldUpdatePosition } = this.state;
|
||||
const { refMediaContainer, usersVideo } = this.props;
|
||||
const { current: mediaContainer } = refMediaContainer;
|
||||
const userLength = usersVideo.length;
|
||||
|
||||
const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector();
|
||||
|
||||
if (webcamBySelector && mediaContainer && shouldUpdatePosition) {
|
||||
if (userLength === 0) this.getVideoListUsersChange();
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
const webcamBySelector = WebcamDraggableOverlay.getWebcamGridBySelector();
|
||||
|
||||
if (webcamBySelector && mediaContainer && this.shouldUpdatePosition) {
|
||||
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
|
||||
const {
|
||||
width: webcamWidth,
|
||||
@ -238,29 +232,37 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
height: mediaHeight,
|
||||
} = mediaContainerRect;
|
||||
|
||||
const lineNum = WebcamDraggableOverlay
|
||||
.getGridLineNum(userLength, webcamWidth, mediaWidth);
|
||||
|
||||
x = mediaWidth - ((webcamWidth + 10) * userLength); // 10 is margin
|
||||
y = mediaHeight - ((webcamHeight + 10) * lineNum); // 10 is margin
|
||||
const x = mediaWidth - ((webcamWidth + 10) * userLength); // 10 is margin
|
||||
const y = mediaHeight - ((webcamHeight + 10)); // 10 is margin
|
||||
|
||||
if (x === 0 && y === 0) return false;
|
||||
|
||||
this.setState({ initialRectPosition: { x, y } });
|
||||
this.setState({
|
||||
initialRectPosition: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastWebcamPosition() {
|
||||
const { refMediaContainer } = this.props;
|
||||
const { refMediaContainer, usersVideo, floatingOverlay } = this.props;
|
||||
const { current: mediaContainer } = refMediaContainer;
|
||||
const { initialRectPosition, userLength, shouldUpdatePosition } = this.state;
|
||||
const {
|
||||
initialRectPosition,
|
||||
dragging,
|
||||
dropOnTop,
|
||||
dropOnBottom,
|
||||
} = this.state;
|
||||
const userLength = usersVideo.length;
|
||||
|
||||
const { x: initX, y: initY } = initialRectPosition;
|
||||
const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector();
|
||||
const webcamBySelector = WebcamDraggableOverlay.getWebcamGridBySelector();
|
||||
|
||||
if (webcamBySelector && mediaContainer && shouldUpdatePosition) {
|
||||
if (webcamBySelector && mediaContainer && this.shouldUpdatePosition) {
|
||||
const webcamBySelectorRect = webcamBySelector.getBoundingClientRect();
|
||||
const {
|
||||
left: webcamLeft,
|
||||
@ -273,28 +275,23 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
top: mediaTop,
|
||||
} = mediaContainerRect;
|
||||
|
||||
const webcamXByMedia = userLength > 1 ? 0 : webcamLeft - mediaLeft;
|
||||
const webcamXByMedia = webcamLeft - mediaLeft;
|
||||
const webcamYByMedia = webcamTop - mediaTop;
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let x = -(initX - webcamXByMedia);
|
||||
x = floatingOverlay ? -((initX - webcamXByMedia) + 10) : x;
|
||||
x = userLength > 1 ? 0 : x;
|
||||
|
||||
if (webcamXByMedia > 0) {
|
||||
x = webcamXByMedia - initX;
|
||||
} else {
|
||||
x = 0 - initX;
|
||||
}
|
||||
if (userLength > 1) x = 0;
|
||||
x = !dragging && webcamXByMedia < 0 ? -initX : x;
|
||||
|
||||
if (webcamYByMedia > 0) {
|
||||
y = webcamYByMedia - initY;
|
||||
} else {
|
||||
y = 0 - initY;
|
||||
}
|
||||
let y = -(initY - webcamYByMedia);
|
||||
y = webcamYByMedia < 0 ? -initY : y;
|
||||
|
||||
if (webcamYByMedia > initY) {
|
||||
y = -10;
|
||||
}
|
||||
y = userLength > 1 && dropOnTop ? -initY : y;
|
||||
y = userLength > 1 && dropOnBottom ? 0 : y;
|
||||
|
||||
y = y < -initY ? -initY : y;
|
||||
y = y > 0 ? 0 : y;
|
||||
|
||||
this.setLastPosition(x, y);
|
||||
}
|
||||
@ -327,7 +324,7 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
|
||||
if (window.innerWidth < 641) {
|
||||
this.setisMinWidth(true);
|
||||
this.setState({ dropOnBottom: true });
|
||||
this.setState({ dropOnTop: true });
|
||||
this.setResetPosition(true);
|
||||
} else if (isMinWidth) {
|
||||
this.setisMinWidth(false);
|
||||
@ -364,14 +361,14 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
resetPosition,
|
||||
} = this.state;
|
||||
|
||||
if (!floatingOverlay) WebcamDraggableOverlay.getOverlayBySelector().style.bottom = 0;
|
||||
if (!floatingOverlay && dropOnTop) WebcamDraggableOverlay.getOverlayBySelector().style.top = 0;
|
||||
|
||||
if (!dragging) this.setState({ dragging: true });
|
||||
if (!showDropZones) this.setState({ showDropZones: true });
|
||||
if (dropOnTop) this.setState({ dropOnTop: false });
|
||||
if (dropOnBottom) this.setState({ dropOnBottom: false });
|
||||
if (!showDropZones) this.setState({ showDropZones: true });
|
||||
|
||||
if (resetPosition) this.setState({ resetPosition: false });
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
handleWebcamDragStop(e, position) {
|
||||
@ -386,6 +383,7 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
if (showDropZones) this.setState({ showDropZones: false });
|
||||
|
||||
this.setLastPosition(x, y);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
dropZoneTopEnterHandler() {
|
||||
@ -454,7 +452,11 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
disableVideo,
|
||||
audioModalIsOpen,
|
||||
refMediaContainer,
|
||||
usersVideo,
|
||||
} = this.props;
|
||||
|
||||
const userLength = usersVideo.length;
|
||||
|
||||
const { current: mediaContainer } = refMediaContainer;
|
||||
|
||||
let mediaContainerRect;
|
||||
@ -482,8 +484,6 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
isMinWidth,
|
||||
} = this.state;
|
||||
|
||||
const webcamBySelectorCount = WebcamDraggableOverlay.getWebcamBySelectorCount();
|
||||
|
||||
const contentClassName = cx({
|
||||
[styles.content]: true,
|
||||
});
|
||||
@ -491,8 +491,7 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
const overlayClassName = cx({
|
||||
[styles.overlay]: true,
|
||||
[styles.overlayRelative]: (dropOnTop || dropOnBottom),
|
||||
[styles.overlayAbsoluteSingle]: (!dropOnTop && !dropOnBottom && webcamBySelectorCount <= 1),
|
||||
[styles.overlayAbsoluteMult]: (!dropOnTop && !dropOnBottom && webcamBySelectorCount > 1),
|
||||
[styles.overlayAbsoluteMult]: (!dropOnTop && !dropOnBottom) && userLength > 1,
|
||||
[styles.hideOverlay]: hideOverlay,
|
||||
[styles.floatingOverlay]: floatingOverlay && (!dropOnTop && !dropOnBottom),
|
||||
[styles.overlayToTop]: dropOnTop,
|
||||
@ -539,12 +538,13 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
onMouseEnter={this.dropZoneTopEnterHandler}
|
||||
onMouseLeave={this.dropZoneTopLeaveHandler}
|
||||
onMouseUp={this.dropZoneTopMouseUpHandler}
|
||||
data-dropzone="dropZoneTop"
|
||||
role="presentation"
|
||||
style={{ height: '100px' }}
|
||||
style={{ height: userLength > 1 ? '50%' : '20%' }}
|
||||
/>
|
||||
<div
|
||||
className={dropZoneBgTopClassName}
|
||||
style={{ height: '100px' }}
|
||||
style={{ height: userLength > 1 ? '50%' : '20%' }}
|
||||
/>
|
||||
|
||||
<Draggable
|
||||
@ -558,6 +558,9 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
>
|
||||
<div
|
||||
className={!swapLayout ? overlayClassName : contentClassName}
|
||||
style={{
|
||||
maxHeight: mediaHeight,
|
||||
}}
|
||||
>
|
||||
{
|
||||
!disableVideo && !audioModalIsOpen
|
||||
@ -565,9 +568,7 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
<VideoProviderContainer
|
||||
cursor={cursor()}
|
||||
swapLayout={swapLayout}
|
||||
mediaHeight={mediaHeight}
|
||||
onMount={this.videoMounted}
|
||||
onUpdate={this.videoUpdated}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@ -578,12 +579,13 @@ export default class WebcamDraggableOverlay extends Component {
|
||||
onMouseEnter={this.dropZoneBottomEnterHandler}
|
||||
onMouseLeave={this.dropZoneBottomLeaveHandler}
|
||||
onMouseUp={this.dropZoneBottomMouseUpHandler}
|
||||
data-dropzone="dropZoneBottom"
|
||||
role="presentation"
|
||||
style={{ height: '100px' }}
|
||||
style={{ height: userLength > 1 ? '50%' : '20%' }}
|
||||
/>
|
||||
<div
|
||||
className={dropZoneBgBottomClassName}
|
||||
style={{ height: '100px' }}
|
||||
style={{ height: userLength > 1 ? '50%' : '20%' }}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -12,7 +12,6 @@ const VideoProviderContainer = ({ children, ...props }) => {
|
||||
export default withTracker(props => ({
|
||||
cursor: props.cursor,
|
||||
swapLayout: props.swapLayout,
|
||||
mediaHeight: props.mediaHeight,
|
||||
meetingId: VideoService.meetingId(),
|
||||
users: VideoService.getAllUsersVideo(),
|
||||
userId: VideoService.userId(),
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import cx from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import { styles } from './styles';
|
||||
import VideoListItem from './video-list-item/component';
|
||||
|
||||
@ -28,18 +29,91 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
// See: https://stackoverflow.com/a/3513565
|
||||
const findOptimalGrid = (canvasWidth, canvasHeight, gutter, aspectRatio, numItems, columns = 1) => {
|
||||
const rows = Math.ceil(numItems / columns);
|
||||
const gutterTotalWidth = (columns - 1) * gutter;
|
||||
const gutterTotalHeight = (rows - 1) * gutter;
|
||||
const usableWidth = canvasWidth - gutterTotalWidth;
|
||||
const usableHeight = canvasHeight - gutterTotalHeight;
|
||||
let cellWidth = Math.floor(usableWidth / columns);
|
||||
let cellHeight = Math.ceil(cellWidth / aspectRatio);
|
||||
if ((cellHeight * rows) > usableHeight) {
|
||||
cellHeight = Math.floor(usableHeight / rows);
|
||||
cellWidth = Math.ceil(cellHeight * aspectRatio);
|
||||
}
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
width: (cellWidth * columns) + gutterTotalWidth,
|
||||
height: (cellHeight * rows) + gutterTotalHeight,
|
||||
filledArea: (cellWidth * cellHeight) * numItems,
|
||||
};
|
||||
};
|
||||
|
||||
const ASPECT_RATIO = 4 / 3;
|
||||
|
||||
class VideoList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
focusedId: false,
|
||||
optimalGrid: {
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
filledArea: 0,
|
||||
},
|
||||
};
|
||||
|
||||
this.ticking = false;
|
||||
this.grid = null;
|
||||
this.canvas = null;
|
||||
this.handleCanvasResize = _.throttle(this.handleCanvasResize.bind(this), 66,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
this.setOptimalGrid = this.setOptimalGrid.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleCanvasResize();
|
||||
window.addEventListener('resize', this.handleCanvasResize, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleCanvasResize, false);
|
||||
}
|
||||
|
||||
setOptimalGrid() {
|
||||
const { users } = this.props;
|
||||
let numItems = users.length;
|
||||
if (numItems < 1 || !this.canvas || !this.grid) {
|
||||
return;
|
||||
}
|
||||
const { focusedId } = this.state;
|
||||
const { width: canvasWidth, height: canvasHeight } = this.canvas.getBoundingClientRect();
|
||||
const gridGutter = parseInt(window.getComputedStyle(this.grid)
|
||||
.getPropertyValue('grid-row-gap'), 10);
|
||||
const hasFocusedItem = numItems > 2 && focusedId;
|
||||
// Has a focused item so we need +3 cells
|
||||
if (hasFocusedItem) {
|
||||
numItems += 3;
|
||||
}
|
||||
const optimalGrid = _.range(1, numItems + 1)
|
||||
.reduce((currentGrid, col) => {
|
||||
const testGrid = findOptimalGrid(
|
||||
canvasWidth, canvasHeight, gridGutter,
|
||||
ASPECT_RATIO, numItems, col,
|
||||
);
|
||||
// We need a minimun of 2 rows and columns for the focused
|
||||
const focusedConstraint = hasFocusedItem ? testGrid.rows > 1 && testGrid.columns > 1 : true;
|
||||
const betterThanCurrent = testGrid.filledArea > currentGrid.filledArea;
|
||||
return focusedConstraint && betterThanCurrent ? testGrid : currentGrid;
|
||||
}, { filledArea: 0 });
|
||||
this.setState({
|
||||
optimalGrid,
|
||||
});
|
||||
}
|
||||
|
||||
handleVideoFocus(id) {
|
||||
@ -50,6 +124,16 @@ class VideoList extends Component {
|
||||
window.dispatchEvent(new Event('videoFocusChange'));
|
||||
}
|
||||
|
||||
handleCanvasResize() {
|
||||
if (!this.ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.ticking = false;
|
||||
this.setOptimalGrid();
|
||||
});
|
||||
}
|
||||
this.ticking = true;
|
||||
}
|
||||
|
||||
renderVideoList() {
|
||||
const {
|
||||
intl,
|
||||
@ -60,7 +144,6 @@ class VideoList extends Component {
|
||||
enableVideoStats,
|
||||
cursor,
|
||||
swapLayout,
|
||||
mediaHeight,
|
||||
} = this.props;
|
||||
const { focusedId } = this.state;
|
||||
|
||||
@ -81,8 +164,7 @@ class VideoList extends Component {
|
||||
<div
|
||||
key={user.id}
|
||||
className={cx({
|
||||
[styles.videoListItem]: !swapLayout,
|
||||
[styles.videoListItemSwapLayout]: swapLayout,
|
||||
[styles.videoListItem]: true,
|
||||
[styles.focused]: focusedId === user.id && users.length > 2,
|
||||
})}
|
||||
style={{
|
||||
@ -93,12 +175,14 @@ class VideoList extends Component {
|
||||
numOfUsers={users.length}
|
||||
user={user}
|
||||
actions={actions}
|
||||
onMount={(videoRef) => { onMount(user.id, videoRef); }}
|
||||
onMount={(videoRef) => {
|
||||
this.handleCanvasResize();
|
||||
onMount(user.id, videoRef);
|
||||
}}
|
||||
getStats={(videoRef, callback) => getStats(user.id, videoRef, callback)}
|
||||
stopGettingStats={() => stopGettingStats(user.id)}
|
||||
enableVideoStats={enableVideoStats}
|
||||
swapLayout={swapLayout}
|
||||
mediaHeight={mediaHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -106,27 +190,36 @@ class VideoList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users, swapLayout } = this.props;
|
||||
const { users } = this.props;
|
||||
const { optimalGrid } = this.state;
|
||||
|
||||
const canvasClassName = cx({
|
||||
[styles.videoCanvas]: !swapLayout,
|
||||
[styles.videoCanvasSwapLayout]: swapLayout,
|
||||
[styles.videoCanvas]: true,
|
||||
});
|
||||
|
||||
const videoListClassName = cx({
|
||||
[styles.videoList]: !swapLayout,
|
||||
[styles.videoListSwapLayout]: swapLayout,
|
||||
[styles.videoList]: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(ref) => { this.canvas = ref; }}
|
||||
ref={(ref) => {
|
||||
this.canvas = ref;
|
||||
}}
|
||||
className={canvasClassName}
|
||||
>
|
||||
{!users.length ? null : (
|
||||
<div
|
||||
ref={(ref) => { this.grid = ref; }}
|
||||
ref={(ref) => {
|
||||
this.grid = ref;
|
||||
}}
|
||||
className={videoListClassName}
|
||||
style={{
|
||||
width: `${optimalGrid.width}px`,
|
||||
height: `${optimalGrid.height}px`,
|
||||
gridTemplateColumns: `repeat(${optimalGrid.columns}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${optimalGrid.rows}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{this.renderVideoList()}
|
||||
</div>
|
||||
|
@ -9,29 +9,15 @@
|
||||
--cam-dropdown-width: 70%;
|
||||
--audio-indicator-width: 1.12rem;
|
||||
--audio-indicator-fs: 75%;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
min-height: var(--video-height);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.videoCanvasSwapLayout {
|
||||
--cam-dropdown-width: 70%;
|
||||
--audio-indicator-width: 1.12rem;
|
||||
--audio-indicator-fs: 75%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -40,12 +26,7 @@
|
||||
display: grid;
|
||||
border-radius: 5px;
|
||||
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
|
||||
grid-template-columns: repeat(auto-fit, var(--video-width));
|
||||
grid-auto-columns: var(--video-width);
|
||||
grid-auto-rows: calc(var(--video-width) / var(--video-ratio));
|
||||
|
||||
grid-auto-flow: dense;
|
||||
grid-gap: 5px;
|
||||
|
||||
align-items: center;
|
||||
@ -56,78 +37,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.videoListSwapLayout {
|
||||
display: grid;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--video-width), 1fr));
|
||||
grid-auto-columns: minmax(var(--video-width), 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: 5px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-auto-flow: row;
|
||||
|
||||
@include mq($medium-up) {
|
||||
grid-gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.videoListItem {
|
||||
display: flex;
|
||||
max-width: fit-content;
|
||||
|
||||
&.focused {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
width: var(--video-width);
|
||||
min-width: var(--video-width);
|
||||
height: calc(var(--video-width) / var(--video-ratio));
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
}
|
||||
|
||||
.videoListItemSwapLayout {
|
||||
display: flex;
|
||||
max-width: -moz-fit-content;
|
||||
max-height: -moz-fit-content;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.focused {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1 / span 2;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
border-radius: 5px;
|
||||
|
||||
background-color: var(--color-gray);
|
||||
|
||||
width: var(--video-width);
|
||||
min-width: var(--video-width);
|
||||
height: calc(var(--video-width) / var(--video-ratio));
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -148,69 +73,6 @@
|
||||
&.talking::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.focused & {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.contentSwapLayout {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
border-radius: 5px;
|
||||
|
||||
background-color: var(--color-gray);
|
||||
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border: 5px solid var(--color-white-with-transparency);
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
:global(.animationsEnabled) & {
|
||||
transition: opacity .1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.talking::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.focused & {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.contentLoading {
|
||||
width: var(--video-width);
|
||||
min-width: var(--video-width);
|
||||
height: calc(var(--video-width) / var(--video-ratio));
|
||||
min-height: calc(var(--video-width) / var(--video-ratio));
|
||||
}
|
||||
|
||||
.contentLoadingSwapLayout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
%media-area {
|
||||
@ -239,8 +101,6 @@
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
||||
&::after {
|
||||
@ -394,4 +254,4 @@
|
||||
|
||||
.voice {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ class VideoListItem extends Component {
|
||||
render() {
|
||||
const { showStats, stats, videoIsReady } = this.state;
|
||||
const {
|
||||
user, numOfUsers, swapLayout, mediaHeight,
|
||||
user, numOfUsers,
|
||||
} = this.props;
|
||||
const availableActions = this.getAvailableActions();
|
||||
const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false;
|
||||
@ -140,22 +140,15 @@ class VideoListItem extends Component {
|
||||
|
||||
return (
|
||||
<div className={cx({
|
||||
[styles.content]: !swapLayout,
|
||||
[styles.contentSwapLayout]: swapLayout,
|
||||
[styles.content]: true,
|
||||
[styles.talking]: user.isTalking,
|
||||
[styles.contentLoading]: !videoIsReady && !swapLayout,
|
||||
[styles.contentLoadingSwapLayout]: !videoIsReady && swapLayout,
|
||||
})}
|
||||
>
|
||||
{!videoIsReady && <div className={styles.connecting} />}
|
||||
<video
|
||||
style={{
|
||||
maxHeight: mediaHeight - 20, // 20 is margin
|
||||
}}
|
||||
muted
|
||||
className={cx({
|
||||
[styles.media]: true,
|
||||
[styles.contentLoading]: !videoIsReady,
|
||||
})}
|
||||
ref={(ref) => { this.videoTag = ref; }}
|
||||
autoPlay
|
||||
|
@ -1,13 +1,6 @@
|
||||
@import "../../stylesheets/variables/_all";
|
||||
|
||||
:root {
|
||||
--video-width: 10vw;
|
||||
--video-height: calc((100vh - calc(var(--navbar-height) + var(--actionsbar-height))) * 0.2);
|
||||
--video-ratio: calc(4 / 3);
|
||||
}
|
||||
|
||||
|
||||
@include mq($small-only) {
|
||||
:root {
|
||||
--video-width: 20vw;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user