diff --git a/bigbluebutton-html5/imports/ui/components/media/styles.scss b/bigbluebutton-html5/imports/ui/components/media/styles.scss index 106191a85a27f80a812fd1e4c69dc89209cb7979..fc83c5f839842f74761f89e701b89020e06989db 100644 --- a/bigbluebutton-html5/imports/ui/components/media/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/media/styles.scss @@ -106,7 +106,6 @@ position: absolute; width: 100%; z-index: 9999; - cursor: grabbing; } .dropZoneTop { diff --git a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx index 42f14f9d926291f3325025ad9841f3133552016e..3513d555247618019a9c32e9a17fbc98d2ae17e0 100644 --- a/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/webcam-draggable-overlay/component.jsx @@ -100,9 +100,11 @@ export default class WebcamDraggableOverlay extends Component { isVideoLoaded: false, isMinWidth: false, userLength: 0, + shouldUpdatePosition: true, }; this.updateWebcamPositionByResize = this.updateWebcamPositionByResize.bind(this); + this.eventVideoFocusChangeListener = this.eventVideoFocusChangeListener.bind(this); this.eventResizeListener = _.throttle( this.updateWebcamPositionByResize, @@ -125,8 +127,10 @@ export default class WebcamDraggableOverlay extends Component { 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.dropZoneTopEnterHandler = this.dropZoneTopEnterHandler.bind(this); this.dropZoneTopLeaveHandler = this.dropZoneTopLeaveHandler.bind(this); @@ -146,6 +150,7 @@ export default class WebcamDraggableOverlay extends Component { && !resetPosition) this.setResetPosition(true); window.addEventListener('resize', this.eventResizeListener); + window.addEventListener('videoFocusChange', this.eventVideoFocusChangeListener); fullscreenChangedEvents.forEach((event) => { document.addEventListener(event, this.handleFullscreenChange); @@ -158,9 +163,18 @@ export default class WebcamDraggableOverlay extends Component { } componentDidUpdate(prevProps, prevState) { - const { userLength } = this.state; - if (userLength !== prevState.userLength) { - this.setLastWebcamPosition({ x: 0 }); + const { swapLayout } = this.props; + const { userLength, lastPosition } = this.state; + const { y } = lastPosition; + // if (prevProps.swapLayout && !swapLayout && userLength === 1) { + // this.setShouldUpdatePosition(false); + // } + if (prevProps.swapLayout && !swapLayout && userLength > 1) { + this.setLastPosition(0, y); + } + if (prevState.userLength === 1 && userLength > 1) { + this.setDropOnBottom(true); + this.setResetPosition(true); } } @@ -170,6 +184,8 @@ export default class WebcamDraggableOverlay extends Component { }); document.removeEventListener('webcamFullscreenButtonChange', this.fullscreenButtonChange); + document.removeEventListener('videoListUsersChange', this.getVideoListUsersChange); + document.removeEventListener('videoFocusChange', this.eventVideoFocusChangeListener); } getVideoListUsersChange() { @@ -189,14 +205,22 @@ export default class WebcamDraggableOverlay extends Component { this.setState({ lastPosition: { x, y } }); } + setShouldUpdatePosition(shouldUpdatePosition) { + this.setState({ shouldUpdatePosition }); + } + + setDropOnBottom(dropOnBottom) { + this.setState({ dropOnBottom }); + } + setInitialReferencePoint() { const { refMediaContainer } = this.props; - const { userLength } = this.state; + const { userLength, shouldUpdatePosition } = this.state; const { current: mediaContainer } = refMediaContainer; const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector(); - if (webcamBySelector && mediaContainer) { + if (webcamBySelector && mediaContainer && shouldUpdatePosition) { if (userLength === 0) this.getVideoListUsersChange(); let x = 0; @@ -231,12 +255,12 @@ export default class WebcamDraggableOverlay extends Component { setLastWebcamPosition() { const { refMediaContainer } = this.props; const { current: mediaContainer } = refMediaContainer; - const { initialRectPosition, userLength } = this.state; + const { initialRectPosition, userLength, shouldUpdatePosition } = this.state; const { x: initX, y: initY } = initialRectPosition; const webcamBySelector = WebcamDraggableOverlay.getWebcamBySelector(); - if (webcamBySelector && mediaContainer) { + if (webcamBySelector && mediaContainer && shouldUpdatePosition) { const webcamBySelectorRect = webcamBySelector.getBoundingClientRect(); const { left: webcamLeft, @@ -260,15 +284,17 @@ export default class WebcamDraggableOverlay extends Component { } else { x = 0 - initX; } - if (webcamXByMedia > initX) x = -(initY - 10); if (userLength > 1) x = 0; - y = webcamYByMedia - (initY - 10); + if (webcamYByMedia > 0) { + y = webcamYByMedia - initY; + } else { + y = 0 - initY; + } if (webcamYByMedia > initY) { y = -10; } - if (webcamYByMedia < 0) y = -(initY - 10); this.setLastPosition(x, y); } @@ -293,13 +319,8 @@ export default class WebcamDraggableOverlay extends Component { isVideoLoaded, isMinWidth, } = this.state; - const webcamBySelectorCount = WebcamDraggableOverlay.getWebcamBySelectorCount(); if (isVideoLoaded) { - if (WebcamDraggableOverlay.isOverlayAbsolute() && webcamBySelectorCount > 1) { - this.setInitialReferencePoint(); - this.setLastWebcamPosition(); - } this.setInitialReferencePoint(); this.setLastWebcamPosition(); } @@ -313,6 +334,13 @@ export default class WebcamDraggableOverlay extends Component { } } + eventVideoFocusChangeListener() { + setTimeout(() => { + this.setInitialReferencePoint(); + this.setLastWebcamPosition(); + }, 500); + } + handleFullscreenChange() { if (document.fullscreenElement || document.webkitFullscreenElement @@ -425,7 +453,20 @@ export default class WebcamDraggableOverlay extends Component { hideOverlay, disableVideo, audioModalIsOpen, + refMediaContainer, } = this.props; + const { current: mediaContainer } = refMediaContainer; + + let mediaContainerRect; + let mediaHeight; + if (mediaContainer) { + mediaContainerRect = mediaContainer.getBoundingClientRect(); + const { + height, + } = mediaContainerRect; + mediaHeight = height; + } + const { dragging, @@ -486,7 +527,7 @@ export default class WebcamDraggableOverlay extends Component { }); const cursor = () => { - if ((!swapLayout || !isFullScreen || !BROWSER_ISMOBILE) && !dragging && !isMinWidth) return 'grab'; + if ((!swapLayout || !isFullScreen || !BROWSER_ISMOBILE || !isMinWidth) && !dragging) return 'grab'; if (dragging) return 'grabbing'; return 'default'; }; @@ -523,6 +564,8 @@ export default class WebcamDraggableOverlay extends Component { ? ( <VideoProviderContainer cursor={cursor()} + swapLayout={swapLayout} + mediaHeight={mediaHeight} onMount={this.videoMounted} onUpdate={this.videoUpdated} /> diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index 2cf9f87fb1591c07086ba7d54326213baa2a820c..804ab478665b907a2924b628c46881d02a70c4cf 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -190,9 +190,13 @@ class VideoProvider extends Component { && peer.peerConnection.getRemoteStreams().length > 0; if (hasLocalStream) { - this.customGetStats(peer.peerConnection, peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true); + this.customGetStats(peer.peerConnection, + peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], + (stats => updateWebcamStats(id, stats)), true); } else if (hasRemoteStream) { - this.customGetStats(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true); + this.customGetStats(peer.peerConnection, + peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], + (stats => updateWebcamStats(id, stats)), true); } }); }, 5000); @@ -833,9 +837,9 @@ class VideoProvider extends Component { let videoBitrate; if (videoStats.packetsReceived > 0) { // Remote video - videoLostPercentage = ((videoStats - .packetsLost / ((videoStats - .packetsLost + videoStats.packetsReceived) * 100)) || 0).toFixed(1); + videoLostPercentage = ((videoStats.packetsLost / ( + (videoStats.packetsLost + videoStats.packetsReceived) * 100 + )) || 0).toFixed(1); videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0); videoLostRecentPercentage = ((videoIntervalPacketsLost / ((videoIntervalPacketsLost + videoIntervalPacketsReceived) * 100)) || 0).toFixed(1); @@ -1005,10 +1009,18 @@ class VideoProvider extends Component { const { socketOpen } = this.state; if (!socketOpen) return null; - const { users, enableVideoStats, cursor } = this.props; + const { + users, + enableVideoStats, + cursor, + swapLayout, + mediaHeight, + } = this.props; return ( <VideoList cursor={cursor} + swapLayout={swapLayout} + mediaHeight={mediaHeight} users={users} onMount={this.createVideoTag} getStats={this.getStats} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx index 9cee75901ca36197ce581157b4c309c3c1aa43d6..00b68f9b2e1f9719982c05fe367095c2dad732ab 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx @@ -11,6 +11,8 @@ 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(), diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx index 7a18d9a44344f52e4a4f0f387cc64074180ac2e2..8ba43a8abf6f628b44ef4eef28a32bf7cea87f44 100644 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/component.jsx @@ -47,12 +47,20 @@ class VideoList extends Component { this.setState({ focusedId: focusedId !== id ? id : false, }, this.handleCanvasResize); - window.dispatchEvent(new Event('resize')); + window.dispatchEvent(new Event('videoFocusChange')); } renderVideoList() { const { - intl, users, onMount, getStats, stopGettingStats, enableVideoStats, cursor, + intl, + users, + onMount, + getStats, + stopGettingStats, + enableVideoStats, + cursor, + swapLayout, + mediaHeight, } = this.props; const { focusedId } = this.state; @@ -73,7 +81,8 @@ class VideoList extends Component { <div key={user.id} className={cx({ - [styles.videoListItem]: true, + [styles.videoListItem]: !swapLayout, + [styles.videoListItemSwapLayout]: swapLayout, [styles.focused]: focusedId === user.id && users.length > 2, })} style={{ @@ -88,6 +97,8 @@ class VideoList extends Component { getStats={(videoRef, callback) => getStats(user.id, videoRef, callback)} stopGettingStats={() => stopGettingStats(user.id)} enableVideoStats={enableVideoStats} + swapLayout={swapLayout} + mediaHeight={mediaHeight} /> </div> ); @@ -95,16 +106,27 @@ class VideoList extends Component { } render() { - const { users } = this.props; + const { users, swapLayout } = this.props; + + const canvasClassName = cx({ + [styles.videoCanvas]: !swapLayout, + [styles.videoCanvasSwapLayout]: swapLayout, + }); + + const videoListClassName = cx({ + [styles.videoList]: !swapLayout, + [styles.videoListSwapLayout]: swapLayout, + }); + return ( <div ref={(ref) => { this.canvas = ref; }} - className={styles.videoCanvas} + className={canvasClassName} > {!users.length ? null : ( <div ref={(ref) => { this.grid = ref; }} - className={styles.videoList} + className={videoListClassName} > {this.renderVideoList()} </div> diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss index 56d924289be289e16d098d63e53aeb2d2be63fa6..154a71077df25be7a8dd7d481a029d4c7b841ae2 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss @@ -11,18 +11,33 @@ --audio-indicator-fs: 75%; position: relative; width: 100%; + min-height: calc(var(--video-width) / var(--video-ratio)); + 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; + align-items: center; + justify-content: center; } .videoList { display: grid; - padding: 10px; border-radius: 5px; min-height: calc(var(--video-width) / var(--video-ratio)); @@ -41,6 +56,24 @@ } } +.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; @@ -62,6 +95,25 @@ 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; min-width: 100%; @@ -107,6 +159,48 @@ } } +.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); @@ -114,8 +208,12 @@ min-height: calc(var(--video-width) / var(--video-ratio)); } -%media-area { +.contentLoadingSwapLayout { + width: 100%; + height: 100%; +} +%media-area { position: relative; height: 100%; width: 100%; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx index 46a1d2b782561425485f8a2cb8a613e211bddb48..29a4e49af6ab9ed55a9c14878478d7aad1b5a669 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx @@ -129,7 +129,9 @@ class VideoListItem extends Component { render() { const { showStats, stats, videoIsReady } = this.state; - const { user, numOfUsers } = this.props; + const { + user, numOfUsers, swapLayout, mediaHeight, + } = this.props; const availableActions = this.getAvailableActions(); const enableVideoMenu = Meteor.settings.public.kurento.enableVideoMenu || false; @@ -138,13 +140,18 @@ class VideoListItem extends Component { return ( <div className={cx({ - [styles.content]: true, + [styles.content]: !swapLayout, + [styles.contentSwapLayout]: swapLayout, [styles.talking]: user.isTalking, - [styles.contentLoading]: !videoIsReady, + [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,