This commit is contained in:
43
react/features/large-video/actionTypes.ts
Normal file
43
react/features/large-video/actionTypes.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Action to select the participant to be displayed in LargeVideo.
|
||||
*
|
||||
* {
|
||||
* type: SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
* participantId: (string|undefined)
|
||||
* }
|
||||
*/
|
||||
export const SELECT_LARGE_VIDEO_PARTICIPANT
|
||||
= 'SELECT_LARGE_VIDEO_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* Action to set the dimensions of the large video.
|
||||
*
|
||||
* {
|
||||
* type: SET_LARGE_VIDEO_DIMENSIONS,
|
||||
* height: number,
|
||||
* width: number
|
||||
* }
|
||||
*/
|
||||
export const SET_LARGE_VIDEO_DIMENSIONS = 'SET_LARGE_VIDEO_DIMENSIONS';
|
||||
|
||||
/**
|
||||
* Action to update the redux store with the current resolution of large video.
|
||||
*
|
||||
* @returns {{
|
||||
* type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
|
||||
* resolution: number
|
||||
* }}
|
||||
*/
|
||||
export const UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
|
||||
= 'UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION';
|
||||
|
||||
/**
|
||||
* Action to set the redux store of the current show me what I'm sharing flag value.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_SEE_WHAT_IS_BEING_SHARED,
|
||||
* seeWhatIsBeingShared: boolean
|
||||
* }}
|
||||
*/
|
||||
export const SET_SEE_WHAT_IS_BEING_SHARED
|
||||
= 'SET_SEE_WHAT_IS_BEING_SHARED';
|
||||
201
react/features/large-video/actions.any.ts
Normal file
201
react/features/large-video/actions.any.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
getDominantSpeakerParticipant,
|
||||
getLocalParticipant,
|
||||
getLocalScreenShareParticipant,
|
||||
getParticipantById,
|
||||
getPinnedParticipant,
|
||||
getRemoteParticipants,
|
||||
getVirtualScreenshareParticipantByOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { getAutoPinSetting } from '../video-layout/functions';
|
||||
|
||||
import {
|
||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
SET_LARGE_VIDEO_DIMENSIONS,
|
||||
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Action to select the participant to be displayed in LargeVideo based on the
|
||||
* participant id provided. If a participant id is not provided, the LargeVideo
|
||||
* participant will be selected based on a variety of factors: If there is a
|
||||
* dominant or pinned speaker, or if there are remote tracks, etc.
|
||||
*
|
||||
* @param {string} participant - The participant id of the user that needs to be
|
||||
* displayed on the large video.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function selectParticipantInLargeVideo(participant?: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
if (isStageFilmstripAvailable(state, 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep Etherpad open.
|
||||
if (state['features/etherpad'].editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantId = participant ?? _electParticipantInLargeVideo(state);
|
||||
const largeVideo = state['features/large-video'];
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
let latestScreenshareParticipantId;
|
||||
|
||||
if (remoteScreenShares?.length) {
|
||||
latestScreenshareParticipantId = remoteScreenShares[remoteScreenShares.length - 1];
|
||||
}
|
||||
|
||||
// When trying to auto pin screenshare, always select the endpoint even though it happens to be
|
||||
// the large video participant in redux (for the reasons listed above in the large video selection
|
||||
// logic above). The auto pin screenshare logic kicks in after the track is added
|
||||
// (which updates the large video participant and selects all endpoints because of the auto tile
|
||||
// view mode). If the screenshare endpoint is not among the forwarded endpoints from the bridge,
|
||||
// it needs to be selected again at this point.
|
||||
if (participantId !== largeVideo.participantId || participantId === latestScreenshareParticipantId) {
|
||||
dispatch({
|
||||
type: SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
participantId
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the currently seen resolution of the video displayed on large video.
|
||||
*
|
||||
* @param {number} resolution - The current resolution (height) of the video.
|
||||
* @returns {{
|
||||
* type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
|
||||
* resolution: number
|
||||
* }}
|
||||
*/
|
||||
export function updateKnownLargeVideoResolution(resolution: number) {
|
||||
return {
|
||||
type: UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION,
|
||||
resolution
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the large video in redux.
|
||||
*
|
||||
* @param {number} height - The height of the large video.
|
||||
* @param {number} width - The width of the large video.
|
||||
* @returns {{
|
||||
* type: SET_LARGE_VIDEO_DIMENSIONS,
|
||||
* height: number,
|
||||
* width: number
|
||||
* }}
|
||||
*/
|
||||
export function setLargeVideoDimensions(height: number, width: number) {
|
||||
return {
|
||||
type: SET_LARGE_VIDEO_DIMENSIONS,
|
||||
height,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent existing remote video track.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState} function.
|
||||
* @private
|
||||
* @returns {(Track|undefined)}
|
||||
*/
|
||||
function _electLastVisibleRemoteParticipant(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
// First we try to get most recent remote video track.
|
||||
for (let i = tracks.length - 1; i >= 0; --i) {
|
||||
const track = tracks[i];
|
||||
|
||||
if (!track.local && track.mediaType === MEDIA_TYPE.VIDEO && track.participantId) {
|
||||
const participant = getParticipantById(state, track.participantId);
|
||||
|
||||
if (participant) {
|
||||
return participant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of the participant who is to be on the stage and
|
||||
* should be displayed in {@code LargeVideo}.
|
||||
*
|
||||
* @param {Object} state - The Redux state from which the participant to be
|
||||
* displayed in {@code LargeVideo} is to be elected.
|
||||
* @private
|
||||
* @returns {(string|undefined)}
|
||||
*/
|
||||
function _electParticipantInLargeVideo(state: IReduxState) {
|
||||
// If a participant is pinned, they will be shown in the LargeVideo (regardless of whether they are local or
|
||||
// remote) when the filmstrip on stage is disabled.
|
||||
let participant = getPinnedParticipant(state);
|
||||
|
||||
if (participant) {
|
||||
return participant.id;
|
||||
}
|
||||
|
||||
const autoPinSetting = getAutoPinSetting();
|
||||
|
||||
if (autoPinSetting) {
|
||||
// when the setting auto_pin_latest_screen_share is true as spot does, prioritize local screenshare
|
||||
if (autoPinSetting === true) {
|
||||
const localScreenShareParticipant = getLocalScreenShareParticipant(state);
|
||||
|
||||
if (localScreenShareParticipant) {
|
||||
return localScreenShareParticipant.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the most recent remote screenshare that was added to the conference.
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
|
||||
|
||||
if (remoteScreenShares?.length) {
|
||||
return remoteScreenShares[remoteScreenShares.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Next, pick the dominant speaker (other than self).
|
||||
participant = getDominantSpeakerParticipant(state);
|
||||
if (participant && !participant.local) {
|
||||
// Return the screensharing participant id associated with this endpoint if multi-stream is enabled and
|
||||
// auto_pin_latest_screen_share setting is disabled.
|
||||
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, participant.id);
|
||||
|
||||
return screenshareParticipant?.id ?? participant.id;
|
||||
}
|
||||
|
||||
// In case this is the local participant.
|
||||
participant = undefined;
|
||||
|
||||
// Next, pick the most recent participant with video.
|
||||
const lastVisibleRemoteParticipant = _electLastVisibleRemoteParticipant(state);
|
||||
|
||||
if (lastVisibleRemoteParticipant) {
|
||||
return lastVisibleRemoteParticipant.id;
|
||||
}
|
||||
|
||||
// Last, select the participant that joined last (other than poltergist or other bot type participants).
|
||||
const participants = [ ...getRemoteParticipants(state).values() ];
|
||||
|
||||
for (let i = participants.length; i > 0 && !participant; i--) {
|
||||
const p = participants[i - 1];
|
||||
|
||||
!p.botType && (participant = p);
|
||||
}
|
||||
if (participant) {
|
||||
return participant.id;
|
||||
}
|
||||
|
||||
return getLocalParticipant(state)?.id;
|
||||
}
|
||||
1
react/features/large-video/actions.native.ts
Normal file
1
react/features/large-video/actions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './actions.any';
|
||||
102
react/features/large-video/actions.web.ts
Normal file
102
react/features/large-video/actions.web.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// @ts-expect-error
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { IStore } from '../app/types';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
|
||||
|
||||
import { SET_SEE_WHAT_IS_BEING_SHARED } from './actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Captures a screenshot of the video displayed on the large video.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function captureLargeVideoScreenshot() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const largeVideo = state['features/large-video'];
|
||||
const promise = Promise.resolve();
|
||||
|
||||
if (!largeVideo?.participantId) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
const participant = getParticipantById(state, largeVideo.participantId);
|
||||
const participantTrack = getVideoTrackByParticipant(state, participant);
|
||||
|
||||
// Participants that join the call video muted do not have a jitsiTrack attached.
|
||||
if (!participantTrack?.jitsiTrack) {
|
||||
return promise;
|
||||
}
|
||||
const videoStream = participantTrack.jitsiTrack.getOriginalStream();
|
||||
|
||||
if (!videoStream) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Get the video element for the large video, cast HTMLElement to HTMLVideoElement to make flow happy.
|
||||
/* eslint-disable-next-line no-extra-parens */
|
||||
const videoElement = document.getElementById('largeVideo') as any;
|
||||
|
||||
if (!videoElement) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Create a HTML canvas and draw video on to the canvas.
|
||||
const [ track ] = videoStream.getVideoTracks();
|
||||
const { height, width } = track.getSettings() ?? track.getConstraints();
|
||||
const canvasElement = document.createElement('canvas');
|
||||
const ctx = canvasElement.getContext('2d');
|
||||
|
||||
canvasElement.style.display = 'none';
|
||||
canvasElement.height = parseInt(height, 10);
|
||||
canvasElement.width = parseInt(width, 10);
|
||||
ctx?.drawImage(videoElement, 0, 0);
|
||||
const dataURL = canvasElement.toDataURL('image/png', 1.0);
|
||||
|
||||
// Cleanup.
|
||||
ctx?.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
||||
canvasElement.remove();
|
||||
|
||||
return Promise.resolve(dataURL);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the large video container based on the dimensions provided.
|
||||
*
|
||||
* @param {number} width - Width that needs to be applied on the large video container.
|
||||
* @param {number} height - Height that needs to be applied on the large video container.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function resizeLargeVideo(width: number, height: number) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const largeVideo = state['features/large-video'];
|
||||
|
||||
if (largeVideo) {
|
||||
const largeVideoContainer = VideoLayout.getLargeVideo();
|
||||
|
||||
largeVideoContainer.updateContainerSize(width, height);
|
||||
largeVideoContainer.resize();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value used to display what is being shared.
|
||||
*
|
||||
* @param {boolean} seeWhatIsBeingShared - The current value.
|
||||
* @returns {{
|
||||
* type: SET_SEE_WHAT_IS_BEING_SHARED,
|
||||
* seeWhatIsBeingShared: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setSeeWhatIsBeingShared(seeWhatIsBeingShared: boolean) {
|
||||
return {
|
||||
type: SET_SEE_WHAT_IS_BEING_SHARED,
|
||||
seeWhatIsBeingShared
|
||||
};
|
||||
}
|
||||
266
react/features/large-video/components/LargeVideo.native.tsx
Normal file
266
react/features/large-video/components/LargeVideo.native.tsx
Normal 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);
|
||||
406
react/features/large-video/components/LargeVideo.web.tsx
Normal file
406
react/features/large-video/components/LargeVideo.web.tsx
Normal 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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
4
react/features/large-video/components/styles.ts
Normal file
4
react/features/large-video/components/styles.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Size for the Avatar.
|
||||
*/
|
||||
export const AVATAR_SIZE = 200;
|
||||
14
react/features/large-video/functions.ts
Normal file
14
react/features/large-video/functions.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
|
||||
/**
|
||||
* Selector for the participant currently displaying on the large video.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getLargeVideoParticipant(state: IReduxState) {
|
||||
const { participantId } = state['features/large-video'];
|
||||
|
||||
return getParticipantById(state, participantId ?? '');
|
||||
}
|
||||
3
react/features/large-video/logger.ts
Normal file
3
react/features/large-video/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/large-video');
|
||||
74
react/features/large-video/middleware.ts
Normal file
74
react/features/large-video/middleware.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PIN_PARTICIPANT
|
||||
} from '../base/participants/actionTypes';
|
||||
import { getDominantSpeakerParticipant, getLocalParticipant } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { isTestModeEnabled } from '../base/testing/functions';
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED
|
||||
} from '../base/tracks/actionTypes';
|
||||
import { TOGGLE_DOCUMENT_EDITING } from '../etherpad/actionTypes';
|
||||
|
||||
import { selectParticipantInLargeVideo } from './actions';
|
||||
import logger from './logger';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Middleware that catches actions related to participants and tracks and
|
||||
* dispatches an action to select a participant depicted by LargeVideo.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
const state = store.getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
||||
|
||||
|
||||
if (dominantSpeaker?.id === action.participant.id) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
if (isTestModeEnabled(state)) {
|
||||
logger.info(`Dominant speaker changed event for: ${action.participant.id}`);
|
||||
}
|
||||
|
||||
if (localParticipant && localParticipant.id !== action.participant.id) {
|
||||
store.dispatch(selectParticipantInLargeVideo());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PIN_PARTICIPANT: {
|
||||
const result = next(action);
|
||||
|
||||
store.dispatch(selectParticipantInLargeVideo(action.participant?.id));
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_LEFT:
|
||||
case TOGGLE_DOCUMENT_EDITING:
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED: {
|
||||
const result = next(action);
|
||||
|
||||
store.dispatch(selectParticipantInLargeVideo());
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
const result = next(action);
|
||||
|
||||
return result;
|
||||
});
|
||||
64
react/features/large-video/reducer.ts
Normal file
64
react/features/large-video/reducer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { PARTICIPANT_ID_CHANGED } from '../base/participants/actionTypes';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
SET_LARGE_VIDEO_DIMENSIONS,
|
||||
SET_SEE_WHAT_IS_BEING_SHARED,
|
||||
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
|
||||
} from './actionTypes';
|
||||
|
||||
export interface ILargeVideoState {
|
||||
height?: number;
|
||||
participantId?: string;
|
||||
resolution?: number;
|
||||
seeWhatIsBeingShared?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
ReducerRegistry.register<ILargeVideoState>('features/large-video', (state = {}, action): ILargeVideoState => {
|
||||
switch (action.type) {
|
||||
|
||||
// When conference is joined, we update ID of local participant from default
|
||||
// 'local' to real ID. However, in large video we might have already
|
||||
// selected 'local' as participant on stage. So in this case we must update
|
||||
// ID of participant on stage to match ID in 'participants' state to avoid
|
||||
// additional changes in state and (re)renders.
|
||||
case PARTICIPANT_ID_CHANGED:
|
||||
if (state.participantId === action.oldValue) {
|
||||
return {
|
||||
...state,
|
||||
participantId: action.newValue
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case SELECT_LARGE_VIDEO_PARTICIPANT:
|
||||
return {
|
||||
...state,
|
||||
participantId: action.participantId
|
||||
};
|
||||
|
||||
case SET_LARGE_VIDEO_DIMENSIONS:
|
||||
return {
|
||||
...state,
|
||||
height: action.height,
|
||||
width: action.width
|
||||
};
|
||||
|
||||
case UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION:
|
||||
return {
|
||||
...state,
|
||||
resolution: action.resolution
|
||||
};
|
||||
|
||||
case SET_SEE_WHAT_IS_BEING_SHARED:
|
||||
return {
|
||||
...state,
|
||||
seeWhatIsBeingShared: action.seeWhatIsBeingShared
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
0
react/features/large-video/subscriber.native.ts
Normal file
0
react/features/large-video/subscriber.native.ts
Normal file
38
react/features/large-video/subscriber.web.ts
Normal file
38
react/features/large-video/subscriber.web.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// @ts-expect-error
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
|
||||
|
||||
import { getLargeVideoParticipant } from './functions';
|
||||
|
||||
/**
|
||||
* Updates the on stage participant video.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/large-video'].participantId,
|
||||
/* listener */ participantId => {
|
||||
VideoLayout.updateLargeVideo(participantId, true);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Schedules a large video update when the streaming status of the track associated with the large video changes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const largeVideoParticipant = getLargeVideoParticipant(state);
|
||||
const videoTrack = getVideoTrackByParticipant(state, largeVideoParticipant);
|
||||
|
||||
return {
|
||||
participantId: largeVideoParticipant?.id,
|
||||
streamingStatus: videoTrack?.streamingStatus
|
||||
};
|
||||
},
|
||||
/* listener */ ({ participantId, streamingStatus }, previousState: any = {}) => {
|
||||
if (streamingStatus !== previousState.streamingStatus) {
|
||||
VideoLayout.updateLargeVideo(participantId, true);
|
||||
}
|
||||
}, {
|
||||
deepEquals: true
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user