init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../app/types';
import { JitsiTrackEvents } from '../../base/lib-jitsi-meet';
import ParticipantView from '../../base/participants/components/ParticipantView.native';
import { getParticipantById, isLocalScreenshareParticipant } from '../../base/participants/functions';
import { trackStreamingStatusChanged } from '../../base/tracks/actions.native';
import { getVideoTrackByParticipant, isLocalVideoTrackDesktop } from '../../base/tracks/functions.native';
import { ITrack } from '../../base/tracks/types';
import { AVATAR_SIZE } from './styles';
/**
* The type of the React {@link Component} props of {@link LargeVideo}.
*/
interface IProps {
/**
* Whether video should be disabled.
*/
_disableVideo: boolean;
/**
* Application's viewport height.
*/
_height: number;
/**
* The ID of the participant (to be) depicted by LargeVideo.
*
* @private
*/
_participantId: string;
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack?: ITrack;
/**
* Application's viewport height.
*/
_width: number;
/**
* Invoked to trigger state changes in Redux.
*/
dispatch: IStore['dispatch'];
/**
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
*/
onClick?: Function;
}
/**
* The type of the React {@link Component} state of {@link LargeVideo}.
*/
interface IState {
/**
* Size for the Avatar. It will be dynamically adjusted based on the
* available size.
*/
avatarSize: number;
/**
* Whether the connectivity indicator will be shown or not. It will be true
* by default, but it may be turned off if there is not enough space.
*/
useConnectivityInfoLabel: boolean;
}
const DEFAULT_STATE = {
avatarSize: AVATAR_SIZE,
useConnectivityInfoLabel: true
};
/** .
* Implements a React {@link Component} which represents the large video (a.k.a.
* The conference participant who is on the local stage) on mobile/React Native.
*
* @augments Component
*/
class LargeVideo extends PureComponent<IProps, IState> {
/**
* Creates new LargeVideo component.
*
* @param {IProps} props - The props of the component.
* @returns {LargeVideo}
*/
constructor(props: IProps) {
super(props);
this.handleTrackStreamingStatusChanged = this.handleTrackStreamingStatusChanged.bind(this);
}
state = {
...DEFAULT_STATE
};
/**
* Handles dimension changes. In case we deem it's too
* small, the connectivity indicator won't be rendered and the avatar
* will occupy the entirety of the available screen state.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: IProps) {
const { _height, _width } = props;
// Get the size, rounded to the nearest even number.
const size = 2 * Math.round(Math.min(_height, _width) / 2);
if (size < AVATAR_SIZE * 1.5) {
return {
avatarSize: size - 15, // Leave some margin.
useConnectivityInfoLabel: false
};
}
return DEFAULT_STATE;
}
/**
* Starts listening for track streaming status updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
// Listen to track streaming status changed event to keep it updated.
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch } = this.props;
if (_videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
/**
* Stops listening for track streaming status updates on the old track and starts listening instead on the new
* track.
*
* @inheritdoc
* @returns {void}
*/
override componentDidUpdate(prevProps: IProps) {
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch } = this.props;
if (prevProps._videoTrack?.jitsiTrack?.getSourceName() !== _videoTrack?.jitsiTrack?.getSourceName()) {
if (prevProps._videoTrack && !prevProps._videoTrack.local) {
prevProps._videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(prevProps._videoTrack.jitsiTrack,
prevProps._videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
if (_videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.on(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
}
/**
* Remove listeners for track streaming status update.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
// TODO: after converting this component to a react function component,
// use a custom hook to update local track streaming status.
const { _videoTrack, dispatch } = this.props;
if (_videoTrack && !_videoTrack.local) {
_videoTrack.jitsiTrack.off(JitsiTrackEvents.TRACK_STREAMING_STATUS_CHANGED,
this.handleTrackStreamingStatusChanged);
dispatch(trackStreamingStatusChanged(_videoTrack.jitsiTrack,
_videoTrack.jitsiTrack.getTrackStreamingStatus()));
}
}
/**
* Handle track streaming status change event by by dispatching an action to update track streaming status for the
* given track in app state.
*
* @param {JitsiTrack} jitsiTrack - The track with streaming status updated.
* @param {JitsiTrackStreamingStatus} streamingStatus - The updated track streaming status.
* @returns {void}
*/
handleTrackStreamingStatusChanged(jitsiTrack: any, streamingStatus: string) {
this.props.dispatch(trackStreamingStatusChanged(jitsiTrack, streamingStatus));
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
avatarSize,
useConnectivityInfoLabel
} = this.state;
const {
_disableVideo,
_participantId,
onClick
} = this.props;
return (
<ParticipantView
avatarSize = { avatarSize }
disableVideo = { _disableVideo }
onPress = { onClick }
participantId = { _participantId }
testHintId = 'org.jitsi.meet.LargeVideo'
useConnectivityInfoLabel = { useConnectivityInfoLabel }
zOrder = { 0 }
zoomEnabled = { true } />
);
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideo's props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { participantId } = state['features/large-video'];
const participant = getParticipantById(state, participantId ?? '');
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
const videoTrack = getVideoTrackByParticipant(state, participant);
let disableVideo = false;
if (isLocalScreenshareParticipant(participant)) {
disableVideo = true;
} else if (participant?.local) {
disableVideo = isLocalVideoTrackDesktop(state);
}
return {
_disableVideo: disableVideo,
_height: height,
_participantId: participantId ?? '',
_videoTrack: videoTrack,
_width: width
};
}
export default connect(_mapStateToProps)(LargeVideo);

View File

@@ -0,0 +1,406 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
// @ts-expect-error
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { IReduxState, IStore } from '../../app/types';
import { isDisplayNameVisible } from '../../base/config/functions.web';
import { VIDEO_TYPE } from '../../base/media/constants';
import { getLocalParticipant } from '../../base/participants/functions';
import Watermarks from '../../base/react/components/web/Watermarks';
import { getHideSelfView } from '../../base/settings/functions.any';
import { getVideoTrackByParticipant } from '../../base/tracks/functions.web';
import { setColorAlpha } from '../../base/util/helpers';
import { isSpotTV } from '../../base/util/spot';
import StageParticipantNameLabel from '../../display-name/components/web/StageParticipantNameLabel';
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
import SharedVideo from '../../shared-video/components/web/SharedVideo';
import Captions from '../../subtitles/components/web/Captions';
import { areClosedCaptionsEnabled } from '../../subtitles/functions.any';
import { setTileView } from '../../video-layout/actions.web';
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
import { isWhiteboardEnabled } from '../../whiteboard/functions';
import { setSeeWhatIsBeingShared } from '../actions.web';
import { getLargeVideoParticipant } from '../functions';
import ScreenSharePlaceholder from './ScreenSharePlaceholder.web';
interface IProps {
/**
* The alpha(opacity) of the background.
*/
_backgroundAlpha?: number;
/**
* The user selected background color.
*/
_customBackgroundColor: string;
/**
* The user selected background image url.
*/
_customBackgroundImageUrl: string;
/**
* Whether the screen-sharing placeholder should be displayed or not.
*/
_displayScreenSharingPlaceholder: boolean;
/**
* Whether or not the hideSelfView is enabled.
*/
_hideSelfView: boolean;
/**
* Prop that indicates whether the chat is open.
*/
_isChatOpen: boolean;
/**
* Whether or not the display name is visible.
*/
_isDisplayNameVisible: boolean;
/**
* Whether or not the local screen share is on large-video.
*/
_isScreenSharing: boolean;
/**
* The large video participant id.
*/
_largeVideoParticipantId: string;
/**
* Local Participant id.
*/
_localParticipantId: string;
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean;
/**
* Whether or not the filmstrip is resizable.
*/
_resizableFilmstrip: boolean;
/**
* Whether or not the screen sharing is visible.
*/
_seeWhatIsBeingShared: boolean;
/**
* Whether or not to show dominant speaker badge.
*/
_showDominantSpeakerBadge: boolean;
/**
* Whether or not to show subtitles button.
*/
_showSubtitles?: boolean;
/**
* The width of the vertical filmstrip (user resized).
*/
_verticalFilmstripWidth?: number | null;
/**
* The max width of the vertical filmstrip.
*/
_verticalViewMaxWidth: number;
/**
* Whether or not the filmstrip is visible.
*/
_visibleFilmstrip: boolean;
/**
* Whether or not the whiteboard is ready to be used.
*/
_whiteboardEnabled: boolean;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
}
/** .
* Implements a React {@link Component} which represents the large video (a.k.a.
* The conference participant who is on the local stage) on Web/React.
*
* @augments Component
*/
class LargeVideo extends Component<IProps> {
_tappedTimeout: number | undefined;
_containerRef: React.RefObject<HTMLDivElement>;
_wrapperRef: React.RefObject<HTMLDivElement>;
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._containerRef = React.createRef<HTMLDivElement>();
this._wrapperRef = React.createRef<HTMLDivElement>();
this._clearTapTimeout = this._clearTapTimeout.bind(this);
this._onDoubleTap = this._onDoubleTap.bind(this);
this._updateLayout = this._updateLayout.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
override componentDidUpdate(prevProps: IProps) {
const {
_visibleFilmstrip,
_isScreenSharing,
_seeWhatIsBeingShared,
_largeVideoParticipantId,
_hideSelfView,
_localParticipantId } = this.props;
if (prevProps._visibleFilmstrip !== _visibleFilmstrip) {
this._updateLayout();
}
if (prevProps._isScreenSharing !== _isScreenSharing && !_isScreenSharing) {
this.props.dispatch(setSeeWhatIsBeingShared(false));
}
if (_isScreenSharing && _seeWhatIsBeingShared) {
VideoLayout.updateLargeVideo(_largeVideoParticipantId, true, true);
}
if (_largeVideoParticipantId === _localParticipantId
&& prevProps._hideSelfView !== _hideSelfView) {
VideoLayout.updateLargeVideo(_largeVideoParticipantId, true, false);
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Element}
*/
override render() {
const {
_displayScreenSharingPlaceholder,
_isChatOpen,
_isDisplayNameVisible,
_noAutoPlayVideo,
_showDominantSpeakerBadge,
_whiteboardEnabled,
_showSubtitles
} = this.props;
const style = this._getCustomStyles();
const className = 'videocontainer';
return (
<div
className = { className }
id = 'largeVideoContainer'
ref = { this._containerRef }
style = { style }>
<SharedVideo />
{_whiteboardEnabled && <Whiteboard />}
<div id = 'etherpad' />
<Watermarks />
<div
id = 'dominantSpeaker'
onTouchEnd = { this._onDoubleTap }>
<div className = 'dynamic-shadow' />
<div id = 'dominantSpeakerAvatarContainer' />
</div>
<div id = 'remotePresenceMessage' />
<span id = 'remoteConnectionMessage' />
<div id = 'largeVideoElementsContainer'>
<div id = 'largeVideoBackgroundContainer' />
{/*
* FIXME: the architecture of elements related to the large
* video and the naming. The background is not part of
* largeVideoWrapper because we are controlling the size of
* the video through largeVideoWrapper. That's why we need
* another container for the background and the
* largeVideoWrapper in order to hide/show them.
*/}
{ _displayScreenSharingPlaceholder ? <ScreenSharePlaceholder /> : <></>}
<div
id = 'largeVideoWrapper'
onTouchEnd = { this._onDoubleTap }
ref = { this._wrapperRef }
role = 'figure' >
<video
autoPlay = { !_noAutoPlayVideo }
id = 'largeVideo'
muted = { true }
playsInline = { true } /* for Safari on iOS to work */ />
</div>
</div>
{ (!interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES && _showSubtitles)
&& <Captions /> }
{
_isDisplayNameVisible
&& (
_showDominantSpeakerBadge && <StageParticipantNameLabel />
)
}
</div>
);
}
/**
* Refreshes the video layout to determine the dimensions of the stage view.
* If the filmstrip is toggled it adds CSS transition classes and removes them
* when the transition is done.
*
* @returns {void}
*/
_updateLayout() {
const { _verticalFilmstripWidth, _resizableFilmstrip } = this.props;
if (_resizableFilmstrip && Number(_verticalFilmstripWidth) >= FILMSTRIP_BREAKPOINT) {
this._containerRef.current?.classList.add('transition');
this._wrapperRef.current?.classList.add('transition');
VideoLayout.refreshLayout();
setTimeout(() => {
this._containerRef?.current && this._containerRef.current.classList.remove('transition');
this._wrapperRef?.current && this._wrapperRef.current.classList.remove('transition');
}, 1000);
} else {
VideoLayout.refreshLayout();
}
}
/**
* Clears the '_tappedTimout'.
*
* @private
* @returns {void}
*/
_clearTapTimeout() {
clearTimeout(this._tappedTimeout);
this._tappedTimeout = undefined;
}
/**
* Creates the custom styles object.
*
* @private
* @returns {Object}
*/
_getCustomStyles() {
const styles: any = {};
const {
_customBackgroundColor,
_customBackgroundImageUrl,
_verticalFilmstripWidth,
_verticalViewMaxWidth,
_visibleFilmstrip
} = this.props;
styles.background = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
if (this.props._backgroundAlpha !== undefined) {
const alphaColor = setColorAlpha(styles.backgroundColor, this.props._backgroundAlpha);
styles.background = alphaColor;
}
if (_customBackgroundImageUrl) {
styles.backgroundImage = `url(${_customBackgroundImageUrl})`;
styles.backgroundSize = 'cover';
}
if (_visibleFilmstrip && Number(_verticalFilmstripWidth) >= FILMSTRIP_BREAKPOINT) {
styles.width = `calc(100% - ${_verticalViewMaxWidth || 0}px)`;
}
return styles;
}
/**
* Sets view to tile view on double tap.
*
* @param {Object} e - The event.
* @private
* @returns {void}
*/
_onDoubleTap(e: React.TouchEvent) {
e.stopPropagation();
e.preventDefault();
if (this._tappedTimeout) {
this._clearTapTimeout();
this.props.dispatch(setTileView(true));
} else {
this._tappedTimeout = window.setTimeout(this._clearTapTimeout, 300);
}
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideo props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const testingConfig = state['features/base/config'].testing;
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
const { isOpen: isChatOpen } = state['features/chat'];
const { width: verticalFilmstripWidth, visible } = state['features/filmstrip'];
const { hideDominantSpeakerBadge } = state['features/base/config'];
const { seeWhatIsBeingShared } = state['features/large-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const largeVideoParticipant = getLargeVideoParticipant(state);
const videoTrack = getVideoTrackByParticipant(state, largeVideoParticipant);
const isLocalScreenshareOnLargeVideo = largeVideoParticipant?.id?.includes(localParticipantId ?? '')
&& videoTrack?.videoType === VIDEO_TYPE.DESKTOP;
return {
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_displayScreenSharingPlaceholder:
Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isSpotTV(state)),
_hideSelfView: getHideSelfView(state),
_isChatOpen: isChatOpen,
_isDisplayNameVisible: isDisplayNameVisible(state),
_isScreenSharing: Boolean(isLocalScreenshareOnLargeVideo),
_largeVideoParticipantId: largeVideoParticipant?.id ?? '',
_localParticipantId: localParticipantId ?? '',
_noAutoPlayVideo: Boolean(testingConfig?.noAutoPlayVideo),
_resizableFilmstrip: isFilmstripResizable(state),
_seeWhatIsBeingShared: Boolean(seeWhatIsBeingShared),
_showDominantSpeakerBadge: !hideDominantSpeakerBadge,
_showSubtitles: areClosedCaptionsEnabled(state)
&& Boolean(state['features/base/settings'].showSubtitlesOnStage),
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_visibleFilmstrip: visible,
_whiteboardEnabled: isWhiteboardEnabled(state)
};
}
export default connect(_mapStateToProps)(LargeVideo);

View File

@@ -0,0 +1,260 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../app/types';
import { shouldDisplayTileView } from '../../video-layout/functions.web';
/**
* Constants to describe the dimensions of the video. Landscape videos
* are wider than they are taller and portrait videos are taller than they
* are wider. The dimensions will determine how {@code LargeVideoBackground}
* will stretch to fill its container.
*
* @type {Object}
*/
export const ORIENTATION = {
LANDSCAPE: 'landscape',
PORTRAIT: 'portrait'
};
/**
* The type of the React {@code Component} props of
* {@link LargeVideoBackgroundCanvas}.
*/
interface IProps {
/**
* Whether or not the layout should change to support tile view mode.
*
* @protected
* @type {boolean}
*/
_shouldDisplayTileView: boolean;
/**
* Additional CSS class names to add to the root of the component.
*/
className: String;
/**
* Whether or not the background should have its visibility hidden.
*/
hidden: boolean;
/**
* Whether or not the video should display flipped horizontally, so left
* becomes right and right becomes left.
*/
mirror: boolean;
/**
* Whether the component should ensure full width of the video is displayed
* (landscape) or full height (portrait).
*/
orientationFit: string;
/**
* The video stream to display.
*/
videoElement: HTMLVideoElement;
}
/**
* Implements a React Component which shows a video element intended to be used
* as a background to fill the empty space of container with another video.
*
* @augments Component
*/
export class LargeVideoBackground extends Component<IProps> {
_canvasEl: HTMLCanvasElement;
_updateCanvasInterval: number | undefined;
/**
* Initializes new {@code LargeVideoBackground} instance.
*
* @param {*} props - The read-only properties with which the new instance
* is to be initialized.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._setCanvasEl = this._setCanvasEl.bind(this);
this._updateCanvas = this._updateCanvas.bind(this);
}
/**
* If the canvas is not hidden, sets the initial interval to update the
* image displayed in the canvas.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
const { _shouldDisplayTileView, hidden, videoElement } = this.props;
if (videoElement && !hidden && !_shouldDisplayTileView) {
this._updateCanvas();
this._setUpdateCanvasInterval();
}
}
/**
* Starts or stops the interval to update the image displayed in the canvas.
*
* @inheritdoc
*/
override componentDidUpdate(prevProps: IProps) {
const wasCanvasUpdating = !prevProps.hidden && !prevProps._shouldDisplayTileView && prevProps.videoElement;
const shouldCanvasUpdating
= !this.props.hidden && !this.props._shouldDisplayTileView && this.props.videoElement;
if (wasCanvasUpdating !== shouldCanvasUpdating) {
if (shouldCanvasUpdating) {
this._clearCanvas();
this._setUpdateCanvasInterval();
} else {
this._clearCanvas();
this._clearUpdateCanvasInterval();
}
}
}
/**
* Clears the interval for updating the image displayed in the canvas.
*
* @inheritdoc
*/
override componentWillUnmount() {
this._clearUpdateCanvasInterval();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
hidden,
mirror
} = this.props;
const classNames = `large-video-background ${mirror ? 'flip-x' : ''} ${hidden ? 'invisible' : ''}`;
return (
<div className = { classNames }>
<canvas
id = 'largeVideoBackground'
ref = { this._setCanvasEl } />
</div>
);
}
/**
* Removes any image displayed on the canvas.
*
* @private
* @returns {void}
*/
_clearCanvas() {
const cavnasContext = this._canvasEl.getContext('2d');
cavnasContext?.clearRect(
0, 0, this._canvasEl.width, this._canvasEl.height);
}
/**
* Clears the interval for updating the image displayed in the canvas.
*
* @private
* @returns {void}
*/
_clearUpdateCanvasInterval() {
clearInterval(this._updateCanvasInterval);
}
/**
* Sets the instance variable for the component's canvas element so it can
* be accessed directly for drawing on.
*
* @param {Object} element - The DOM element for the component's canvas.
* @private
* @returns {void}
*/
_setCanvasEl(element: HTMLCanvasElement) {
this._canvasEl = element;
}
/**
* Starts the interval for updating the image displayed in the canvas.
*
* @private
* @returns {void}
*/
_setUpdateCanvasInterval() {
this._clearUpdateCanvasInterval();
this._updateCanvasInterval = window.setInterval(this._updateCanvas, 200);
}
/**
* Draws the current frame of the passed in video element onto the canvas.
*
* @private
* @returns {void}
*/
_updateCanvas() {
// On Electron 7 there is a memory leak if we try to draw into a hidden canvas that is part of the DOM tree.
// See: https://github.com/electron/electron/issues/22417
// Trying to detect all the cases when the page will be hidden because of something not in our control
// (for example when the page is loaded in an iframe which is hidden due to the host page styles) to solve
// the memory leak. Currently we are not handling the use case when the page is hidden with visibility:hidden
// because we don't have a good way to do it.
// All other cases when the canvas is not visible are handled through the component props
// (hidden, _shouldDisplayTileView).
if (!this._canvasEl?.offsetParent || window.innerHeight === 0 || window.innerWidth === 0) {
return;
}
const { videoElement } = this.props;
const { videoWidth, videoHeight } = videoElement;
const {
height: canvasHeight,
width: canvasWidth
} = this._canvasEl;
const canvasContext = this._canvasEl.getContext('2d');
if (this.props.orientationFit === ORIENTATION.LANDSCAPE) {
const heightScaledToFit = (canvasWidth / videoWidth) * videoHeight;
canvasContext?.drawImage(
videoElement as any, 0, 0, canvasWidth, heightScaledToFit);
} else {
const widthScaledToFit = (canvasHeight / videoHeight) * videoWidth;
canvasContext?.drawImage(
videoElement as any, 0, 0, widthScaledToFit, canvasHeight);
}
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideoBackground props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _shouldDisplayTileView: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
return {
_shouldDisplayTileView: shouldDisplayTileView(state)
};
}
export default connect(_mapStateToProps)(LargeVideoBackground);

View File

@@ -0,0 +1,100 @@
import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { useStore } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { translate } from '../../base/i18n/functions';
import { setSeeWhatIsBeingShared } from '../actions.web';
const useStyles = makeStyles()(theme => {
return {
overlayContainer: {
width: '100%',
height: '100%',
backgroundColor: theme.palette.ui02,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
top: 0,
left: 0,
zIndex: 2
},
content: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
laptop: {
width: '88px',
height: '56px',
boxSizing: 'border-box',
border: '3px solid',
borderColor: theme.palette.text01,
borderRadius: '6px'
},
laptopStand: {
width: '40px',
height: '4px',
backgroundColor: theme.palette.text01,
boxSizing: 'border-box',
borderRadius: '6px',
marginTop: '4px'
},
sharingMessage: {
fontStyle: 'normal',
fontWeight: 600,
fontSize: '1.25rem',
lineHeight: '1.75rem',
marginTop: '24px',
letterSpacing: '-0.012em',
color: theme.palette.text01
},
showSharing: {
fontStyle: 'normal',
fontWeight: 600,
fontSize: '0.875rem',
lineHeight: '1.25rem',
height: '20px',
marginTop: '16px',
color: theme.palette.link01,
cursor: 'pointer',
'&:hover': {
color: theme.palette.link01Hover
}
}
};
});
/**
* Component that displays a placeholder for when the screen is shared.
* * @param {Function} t - Function which translate strings.
*
* @returns {ReactElement}
*/
const ScreenSharePlaceholder: React.FC<WithTranslation> = ({ t }) => {
const { classes } = useStyles();
const store = useStore();
const updateShowMeWhatImSharing = useCallback(() => {
store.dispatch(setSeeWhatIsBeingShared(true));
}, []);
return (
<div className = { classes.overlayContainer }>
<div className = { classes.content }>
<div className = { classes.laptop } />
<div className = { classes.laptopStand } />
<span className = { classes.sharingMessage }>{ t('largeVideo.screenIsShared') }</span>
<span
className = { classes.showSharing }
onClick = { updateShowMeWhatImSharing }
role = 'button'>{ t('largeVideo.showMeWhatImSharing') }</span>
</div>
</div>
);
};
export default translate(ScreenSharePlaceholder);

View File

@@ -0,0 +1,4 @@
/**
* Size for the Avatar.
*/
export const AVATAR_SIZE = 200;