2017-07-25 03:29:34 +08:00
|
|
|
import React from 'react';
|
2020-05-26 04:00:13 +08:00
|
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
2019-02-08 01:47:28 +08:00
|
|
|
import PropTypes from 'prop-types';
|
2019-04-13 04:59:48 +08:00
|
|
|
import _ from 'lodash';
|
2019-07-23 00:59:34 +08:00
|
|
|
import FullscreenButtonContainer from '../fullscreen-button/container';
|
2021-06-03 02:31:20 +08:00
|
|
|
import SwitchButtonContainer from './switch-button/container';
|
2018-04-30 19:55:54 +08:00
|
|
|
import { styles } from './styles';
|
2019-08-03 05:32:42 +08:00
|
|
|
import AutoplayOverlay from '../media/autoplay-overlay/component';
|
2019-09-07 02:58:22 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
|
|
|
import playAndRetry from '/imports/utils/mediaElementPlayRetry';
|
2021-07-21 21:38:48 +08:00
|
|
|
import { notify } from '/imports/ui/services/notification';
|
2020-12-10 06:00:54 +08:00
|
|
|
import {
|
|
|
|
SCREENSHARE_MEDIA_ELEMENT_NAME,
|
|
|
|
screenshareHasEnded,
|
|
|
|
screenshareHasStarted,
|
|
|
|
getMediaElement,
|
|
|
|
attachLocalPreviewStream,
|
|
|
|
} from '/imports/ui/components/screenshare/service';
|
|
|
|
import {
|
2021-02-06 06:18:39 +08:00
|
|
|
isStreamStateUnhealthy,
|
2020-12-10 06:00:54 +08:00
|
|
|
subscribeToStreamStateChange,
|
|
|
|
unsubscribeFromStreamStateChange,
|
|
|
|
} from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service';
|
2021-08-12 00:19:24 +08:00
|
|
|
import { ACTIONS } from '/imports/ui/components/layout/enums';
|
2018-04-30 19:55:54 +08:00
|
|
|
|
2019-02-07 05:12:59 +08:00
|
|
|
const intlMessages = defineMessages({
|
|
|
|
screenShareLabel: {
|
|
|
|
id: 'app.screenshare.screenShareLabel',
|
|
|
|
description: 'screen share area element label',
|
|
|
|
},
|
2021-06-03 02:31:20 +08:00
|
|
|
presenterLoadingLabel: {
|
|
|
|
id: 'app.screenshare.presenterLoadingLabel',
|
|
|
|
},
|
2021-07-10 00:36:23 +08:00
|
|
|
viewerLoadingLabel: {
|
|
|
|
id: 'app.screenshare.viewerLoadingLabel',
|
|
|
|
},
|
2021-06-03 02:31:20 +08:00
|
|
|
presenterSharingLabel: {
|
|
|
|
id: 'app.screenshare.presenterSharingLabel',
|
|
|
|
},
|
2019-08-03 05:32:42 +08:00
|
|
|
autoplayBlockedDesc: {
|
|
|
|
id: 'app.media.screenshare.autoplayBlockedDesc',
|
|
|
|
},
|
|
|
|
autoplayAllowLabel: {
|
|
|
|
id: 'app.media.screenshare.autoplayAllowLabel',
|
|
|
|
},
|
2021-07-21 21:38:48 +08:00
|
|
|
screenshareStarted: {
|
|
|
|
id: 'app.media.screenshare.start',
|
|
|
|
description: 'toast to show when a screenshare has started',
|
|
|
|
},
|
|
|
|
screenshareEnded: {
|
|
|
|
id: 'app.media.screenshare.end',
|
|
|
|
description: 'toast to show when a screenshare has ended',
|
|
|
|
},
|
2019-02-07 05:12:59 +08:00
|
|
|
});
|
|
|
|
|
2019-07-24 03:56:39 +08:00
|
|
|
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
|
|
|
|
|
2019-02-07 05:12:59 +08:00
|
|
|
class ScreenshareComponent extends React.Component {
|
2021-08-09 22:24:02 +08:00
|
|
|
static renderScreenshareContainerInside(mainText) {
|
|
|
|
return (
|
|
|
|
<div className={styles.screenshareContainerInside}>
|
|
|
|
<h1 className={styles.mainText}>{mainText}</h1>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-05-15 21:13:15 +08:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
2018-05-15 22:24:13 +08:00
|
|
|
loaded: false,
|
2019-08-03 05:32:42 +08:00
|
|
|
autoplayBlocked: false,
|
2020-12-10 06:00:54 +08:00
|
|
|
isStreamHealthy: false,
|
2021-06-03 02:31:20 +08:00
|
|
|
switched: false,
|
2018-05-15 21:13:15 +08:00
|
|
|
};
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
this.onLoadedData = this.onLoadedData.bind(this);
|
2019-08-03 05:32:42 +08:00
|
|
|
this.handleAllowAutoplay = this.handleAllowAutoplay.bind(this);
|
|
|
|
this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this);
|
|
|
|
this.failedMediaElements = [];
|
2020-12-10 06:00:54 +08:00
|
|
|
this.onStreamStateChange = this.onStreamStateChange.bind(this);
|
2021-06-03 02:31:20 +08:00
|
|
|
this.onSwitched = this.onSwitched.bind(this);
|
2018-05-15 21:13:15 +08:00
|
|
|
}
|
2019-01-17 00:50:24 +08:00
|
|
|
|
2017-07-25 03:29:34 +08:00
|
|
|
componentDidMount() {
|
2021-08-10 03:06:31 +08:00
|
|
|
const {
|
|
|
|
getSwapLayout,
|
|
|
|
toggleSwapLayout,
|
|
|
|
layoutContextDispatch,
|
|
|
|
intl,
|
|
|
|
} = this.props;
|
2021-07-21 21:38:48 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
screenshareHasStarted();
|
|
|
|
// Autoplay failure handling
|
2019-08-03 05:32:42 +08:00
|
|
|
window.addEventListener('screensharePlayFailed', this.handlePlayElementFailed);
|
2020-12-10 06:00:54 +08:00
|
|
|
// Stream health state tracker to propagate UI changes on reconnections
|
|
|
|
subscribeToStreamStateChange('screenshare', this.onStreamStateChange);
|
|
|
|
// Attaches the local stream if it exists to serve as the local presenter preview
|
|
|
|
attachLocalPreviewStream(getMediaElement());
|
2021-07-21 21:38:48 +08:00
|
|
|
|
|
|
|
notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop');
|
2021-08-10 03:06:31 +08:00
|
|
|
|
|
|
|
if (getSwapLayout()) toggleSwapLayout(layoutContextDispatch);
|
2017-07-25 03:29:34 +08:00
|
|
|
}
|
2019-01-17 00:50:24 +08:00
|
|
|
|
2020-08-08 04:32:46 +08:00
|
|
|
componentDidUpdate(prevProps) {
|
2019-04-16 05:39:07 +08:00
|
|
|
const {
|
2020-12-10 06:00:54 +08:00
|
|
|
isPresenter,
|
2019-04-16 05:39:07 +08:00
|
|
|
} = this.props;
|
2020-08-08 04:32:46 +08:00
|
|
|
if (isPresenter && !prevProps.isPresenter) {
|
2020-12-10 06:00:54 +08:00
|
|
|
screenshareHasEnded();
|
2018-03-15 02:28:28 +08:00
|
|
|
}
|
|
|
|
}
|
2019-01-17 00:50:24 +08:00
|
|
|
|
2018-03-15 02:28:28 +08:00
|
|
|
componentWillUnmount() {
|
2021-08-12 03:38:13 +08:00
|
|
|
const { intl, fullscreenContext, layoutContextDispatch } = this.props;
|
2020-12-10 06:00:54 +08:00
|
|
|
screenshareHasEnded();
|
2019-08-03 05:32:42 +08:00
|
|
|
window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed);
|
2020-12-10 06:00:54 +08:00
|
|
|
unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange);
|
2021-07-21 21:38:48 +08:00
|
|
|
|
|
|
|
notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop');
|
2021-08-12 00:19:24 +08:00
|
|
|
|
|
|
|
if (fullscreenContext) {
|
|
|
|
layoutContextDispatch({
|
|
|
|
type: ACTIONS.SET_FULLSCREEN_ELEMENT,
|
|
|
|
value: {
|
|
|
|
element: '',
|
|
|
|
group: '',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2020-12-10 06:00:54 +08:00
|
|
|
}
|
|
|
|
|
2019-08-03 05:32:42 +08:00
|
|
|
handleAllowAutoplay() {
|
|
|
|
const { autoplayBlocked } = this.state;
|
|
|
|
|
2019-09-07 02:58:22 +08:00
|
|
|
logger.info({
|
|
|
|
logCode: 'screenshare_autoplay_allowed',
|
|
|
|
}, 'Screenshare media autoplay allowed by the user');
|
|
|
|
|
2019-08-03 05:32:42 +08:00
|
|
|
window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed);
|
|
|
|
while (this.failedMediaElements.length) {
|
|
|
|
const mediaElement = this.failedMediaElements.shift();
|
|
|
|
if (mediaElement) {
|
2019-09-07 02:58:22 +08:00
|
|
|
const played = playAndRetry(mediaElement);
|
|
|
|
if (!played) {
|
|
|
|
logger.error({
|
|
|
|
logCode: 'screenshare_autoplay_handling_failed',
|
|
|
|
}, 'Screenshare autoplay handling failed to play media');
|
|
|
|
} else {
|
|
|
|
logger.info({
|
|
|
|
logCode: 'screenshare_viewer_media_play_success',
|
|
|
|
}, 'Screenshare viewer media played successfully');
|
|
|
|
}
|
2019-08-03 05:32:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (autoplayBlocked) { this.setState({ autoplayBlocked: false }); }
|
|
|
|
}
|
|
|
|
|
|
|
|
handlePlayElementFailed(e) {
|
|
|
|
const { mediaElement } = e.detail;
|
|
|
|
const { autoplayBlocked } = this.state;
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
this.failedMediaElements.push(mediaElement);
|
|
|
|
if (!autoplayBlocked) {
|
2019-09-07 02:58:22 +08:00
|
|
|
logger.info({
|
|
|
|
logCode: 'screenshare_autoplay_prompt',
|
|
|
|
}, 'Prompting user for action to play screenshare media');
|
|
|
|
|
2019-08-03 05:32:42 +08:00
|
|
|
this.setState({ autoplayBlocked: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-09 22:24:02 +08:00
|
|
|
onStreamStateChange(event) {
|
|
|
|
const { streamState } = event.detail;
|
|
|
|
const { isStreamHealthy } = this.state;
|
|
|
|
|
|
|
|
const newHealthState = !isStreamStateUnhealthy(streamState);
|
|
|
|
event.stopPropagation();
|
|
|
|
if (newHealthState !== isStreamHealthy) {
|
|
|
|
this.setState({ isStreamHealthy: newHealthState });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onLoadedData() {
|
|
|
|
this.setState({ loaded: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
onSwitched() {
|
|
|
|
this.setState((prevState) => ({ switched: !prevState.switched }));
|
|
|
|
}
|
|
|
|
|
2019-01-08 02:12:28 +08:00
|
|
|
renderFullscreenButton() {
|
2021-08-12 00:29:09 +08:00
|
|
|
const { intl, fullscreenElementId, fullscreenContext } = this.props;
|
2019-07-24 03:56:39 +08:00
|
|
|
|
|
|
|
if (!ALLOW_FULLSCREEN) return null;
|
2019-02-07 05:12:59 +08:00
|
|
|
|
|
|
|
return (
|
2019-04-24 22:20:53 +08:00
|
|
|
<FullscreenButtonContainer
|
2019-04-13 04:59:48 +08:00
|
|
|
key={_.uniqueId('fullscreenButton-')}
|
2019-02-07 05:12:59 +08:00
|
|
|
elementName={intl.formatMessage(intlMessages.screenShareLabel)}
|
2019-07-24 03:56:39 +08:00
|
|
|
fullscreenRef={this.screenshareContainer}
|
2021-07-07 03:27:28 +08:00
|
|
|
elementId={fullscreenElementId}
|
2021-08-12 00:29:09 +08:00
|
|
|
isFullscreen={fullscreenContext}
|
2019-07-24 03:56:39 +08:00
|
|
|
dark
|
2019-02-07 05:12:59 +08:00
|
|
|
/>
|
|
|
|
);
|
2019-01-08 02:12:28 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
renderAutoplayOverlay() {
|
2019-08-03 05:32:42 +08:00
|
|
|
const { intl } = this.props;
|
2019-01-08 02:12:28 +08:00
|
|
|
|
2017-07-25 03:29:34 +08:00
|
|
|
return (
|
2020-12-10 06:00:54 +08:00
|
|
|
<AutoplayOverlay
|
|
|
|
key={_.uniqueId('screenshareAutoplayOverlay')}
|
|
|
|
autoplayBlockedDesc={intl.formatMessage(intlMessages.autoplayBlockedDesc)}
|
|
|
|
autoplayAllowLabel={intl.formatMessage(intlMessages.autoplayAllowLabel)}
|
|
|
|
handleAllowAutoplay={this.handleAllowAutoplay}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-06-03 02:31:20 +08:00
|
|
|
renderSwitchButton() {
|
|
|
|
const { switched } = this.state;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SwitchButtonContainer
|
|
|
|
handleSwitch={this.onSwitched}
|
|
|
|
switched={switched}
|
|
|
|
dark
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderVideo(switched) {
|
2021-07-10 00:36:23 +08:00
|
|
|
const { isGloballyBroadcasting } = this.props;
|
|
|
|
|
2021-06-03 02:31:20 +08:00
|
|
|
return (
|
|
|
|
<video
|
|
|
|
id={SCREENSHARE_MEDIA_ELEMENT_NAME}
|
|
|
|
key={SCREENSHARE_MEDIA_ELEMENT_NAME}
|
|
|
|
style={switched
|
|
|
|
? { maxHeight: '100%', width: '100%', height: '100%' }
|
|
|
|
: { maxHeight: '25%', width: '25%', height: '25%' }}
|
2021-07-10 00:36:23 +08:00
|
|
|
className={!isGloballyBroadcasting ? styles.unhealthyStream : null}
|
2021-06-03 02:31:20 +08:00
|
|
|
playsInline
|
|
|
|
onLoadedData={this.onLoadedData}
|
|
|
|
ref={(ref) => {
|
|
|
|
this.videoTag = ref;
|
|
|
|
}}
|
|
|
|
muted
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScreensharePresenter() {
|
2021-08-02 22:35:46 +08:00
|
|
|
const { switched } = this.state;
|
2021-06-03 02:31:20 +08:00
|
|
|
const { isGloballyBroadcasting, intl } = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={switched ? styles.screenshareContainer : styles.screenshareContainerPresenter}
|
|
|
|
key="screenshareContainer"
|
|
|
|
ref={(ref) => { this.screenshareContainer = ref; }}
|
|
|
|
>
|
2021-08-02 22:35:46 +08:00
|
|
|
{isGloballyBroadcasting && this.renderSwitchButton()}
|
2021-06-03 02:31:20 +08:00
|
|
|
{this.renderVideo(switched)}
|
|
|
|
|
2021-08-09 22:24:02 +08:00
|
|
|
{
|
|
|
|
isGloballyBroadcasting
|
|
|
|
? (
|
|
|
|
<div>
|
|
|
|
{!switched
|
|
|
|
&& ScreenshareComponent.renderScreenshareContainerInside(
|
|
|
|
intl.formatMessage(intlMessages.presenterSharingLabel),
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
: ScreenshareComponent.renderScreenshareContainerInside(
|
|
|
|
intl.formatMessage(intlMessages.presenterLoadingLabel),
|
|
|
|
)
|
2021-06-03 02:31:20 +08:00
|
|
|
}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScreenshareDefault() {
|
2021-07-12 22:01:23 +08:00
|
|
|
const { intl } = this.props;
|
2021-08-12 00:29:09 +08:00
|
|
|
const { loaded } = this.state;
|
2021-06-03 02:31:20 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={styles.screenshareContainer}
|
|
|
|
key="screenshareContainer"
|
|
|
|
ref={(ref) => {
|
|
|
|
this.screenshareContainer = ref;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{loaded && this.renderFullscreenButton()}
|
|
|
|
{this.renderVideo(true)}
|
2021-07-10 00:36:23 +08:00
|
|
|
|
|
|
|
<div className={styles.screenshareContainerDefault}>
|
2021-08-09 22:24:02 +08:00
|
|
|
{
|
|
|
|
!loaded
|
|
|
|
? ScreenshareComponent.renderScreenshareContainerInside(
|
|
|
|
intl.formatMessage(intlMessages.viewerLoadingLabel),
|
|
|
|
)
|
|
|
|
: null
|
2021-07-10 00:36:23 +08:00
|
|
|
}
|
|
|
|
</div>
|
2021-06-03 02:31:20 +08:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-07-25 03:29:34 +08:00
|
|
|
render() {
|
2021-06-03 02:31:20 +08:00
|
|
|
const { loaded, autoplayBlocked, isStreamHealthy } = this.state;
|
2021-07-07 03:27:28 +08:00
|
|
|
const {
|
|
|
|
isPresenter,
|
|
|
|
isGloballyBroadcasting,
|
|
|
|
top,
|
|
|
|
left,
|
2021-07-27 04:28:05 +08:00
|
|
|
right,
|
2021-07-07 03:27:28 +08:00
|
|
|
width,
|
|
|
|
height,
|
|
|
|
zIndex,
|
|
|
|
} = this.props;
|
2021-02-06 06:18:39 +08:00
|
|
|
|
2021-07-10 00:36:23 +08:00
|
|
|
// Conditions to render the (re)connecting dots and the unhealthy stream
|
2021-02-06 06:18:39 +08:00
|
|
|
// grayscale:
|
|
|
|
// 1 - The local media tag has not received any stream data yet
|
|
|
|
// 2 - The user is a presenter and the stream wasn't globally broadcasted yet
|
|
|
|
// 3 - The media was loaded, the stream was globally broadcasted BUT the stream
|
|
|
|
// state transitioned to an unhealthy stream. tl;dr: screen sharing reconnection
|
|
|
|
const shouldRenderConnectingState = !loaded
|
|
|
|
|| (isPresenter && !isGloballyBroadcasting)
|
2021-08-09 22:24:02 +08:00
|
|
|
|| (!isStreamHealthy && loaded && isGloballyBroadcasting);
|
2019-01-08 02:12:28 +08:00
|
|
|
|
2017-07-25 03:29:34 +08:00
|
|
|
return (
|
2021-06-15 19:51:44 +08:00
|
|
|
<div
|
|
|
|
style={
|
2021-08-05 12:22:07 +08:00
|
|
|
{
|
|
|
|
position: 'absolute',
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
right,
|
|
|
|
height,
|
|
|
|
width,
|
|
|
|
zIndex,
|
|
|
|
backgroundColor: '#06172A',
|
|
|
|
}
|
2021-06-15 19:51:44 +08:00
|
|
|
}
|
|
|
|
>
|
|
|
|
{(shouldRenderConnectingState)
|
|
|
|
&& (
|
|
|
|
<div
|
|
|
|
key={_.uniqueId('screenshareArea-')}
|
2021-07-10 00:36:23 +08:00
|
|
|
className={styles.spinnerWrapper}
|
2021-06-15 19:51:44 +08:00
|
|
|
data-test="screenshareConnecting"
|
2021-07-10 00:36:23 +08:00
|
|
|
>
|
|
|
|
<div className={styles.spinner}>
|
|
|
|
<div className={styles.bounce1} />
|
|
|
|
<div className={styles.bounce2} />
|
|
|
|
<div />
|
|
|
|
</div>
|
|
|
|
</div>
|
2021-06-15 19:51:44 +08:00
|
|
|
)}
|
|
|
|
{autoplayBlocked ? this.renderAutoplayOverlay() : null}
|
|
|
|
{isPresenter ? this.renderScreensharePresenter() : this.renderScreenshareDefault()}
|
|
|
|
</div>
|
2017-07-25 03:29:34 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2019-02-07 05:12:59 +08:00
|
|
|
|
2021-08-05 15:26:03 +08:00
|
|
|
export default injectIntl(ScreenshareComponent);
|
2019-02-07 05:12:59 +08:00
|
|
|
|
|
|
|
ScreenshareComponent.propTypes = {
|
2021-08-09 22:24:02 +08:00
|
|
|
intl: PropTypes.shape({
|
|
|
|
formatMessage: PropTypes.func.isRequired,
|
|
|
|
}).isRequired,
|
2019-02-08 01:47:28 +08:00
|
|
|
isPresenter: PropTypes.bool.isRequired,
|
2019-02-07 05:12:59 +08:00
|
|
|
};
|