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-27 00:48:51 +08:00
|
|
|
import FullscreenService from '../fullscreen-button/service';
|
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-02-15 16:54:02 +08:00
|
|
|
import PollingContainer from '/imports/ui/components/polling/container';
|
2021-02-24 01:04:00 +08:00
|
|
|
import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
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';
|
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',
|
|
|
|
},
|
|
|
|
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',
|
|
|
|
},
|
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 {
|
2018-05-15 21:13:15 +08:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
2018-05-15 22:24:13 +08:00
|
|
|
loaded: false,
|
2019-07-27 00:48:51 +08:00
|
|
|
isFullscreen: 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-07-27 00:48:51 +08:00
|
|
|
this.onFullscreenChange = this.onFullscreenChange.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() {
|
2020-12-10 06:00:54 +08:00
|
|
|
screenshareHasStarted();
|
2019-07-30 23:03:29 +08:00
|
|
|
this.screenshareContainer.addEventListener('fullscreenchange', this.onFullscreenChange);
|
2020-12-10 06:00:54 +08:00
|
|
|
// 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());
|
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() {
|
2019-04-16 05:39:07 +08:00
|
|
|
const {
|
2020-06-11 21:16:22 +08:00
|
|
|
getSwapLayout,
|
|
|
|
shouldEnableSwapLayout,
|
|
|
|
toggleSwapLayout,
|
2019-04-16 05:39:07 +08:00
|
|
|
} = this.props;
|
2020-06-11 21:16:22 +08:00
|
|
|
const layoutSwapped = getSwapLayout() && shouldEnableSwapLayout();
|
|
|
|
if (layoutSwapped) toggleSwapLayout();
|
2020-12-10 06:00:54 +08:00
|
|
|
screenshareHasEnded();
|
2019-07-30 23:03:29 +08:00
|
|
|
this.screenshareContainer.removeEventListener('fullscreenchange', this.onFullscreenChange);
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
onStreamStateChange (event) {
|
|
|
|
const { streamState } = event.detail;
|
|
|
|
const { isStreamHealthy } = this.state;
|
|
|
|
|
2021-02-06 06:18:39 +08:00
|
|
|
const newHealthState = !isStreamStateUnhealthy(streamState);
|
2020-12-10 06:00:54 +08:00
|
|
|
event.stopPropagation();
|
|
|
|
if (newHealthState !== isStreamHealthy) {
|
|
|
|
this.setState({ isStreamHealthy: newHealthState });
|
|
|
|
}
|
2018-03-15 02:28:28 +08:00
|
|
|
}
|
2019-01-17 00:50:24 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
onLoadedData() {
|
2018-05-15 22:24:13 +08:00
|
|
|
this.setState({ loaded: true });
|
2018-05-15 21:13:15 +08:00
|
|
|
}
|
2017-07-25 03:29:34 +08:00
|
|
|
|
2021-06-03 02:31:20 +08:00
|
|
|
onSwitched() {
|
|
|
|
this.setState(prevState => ({ switched: !prevState.switched }));
|
|
|
|
}
|
|
|
|
|
2019-07-27 00:48:51 +08:00
|
|
|
onFullscreenChange() {
|
2021-02-24 01:04:00 +08:00
|
|
|
const { layoutContextDispatch } = this.props;
|
2019-07-27 00:48:51 +08:00
|
|
|
const { isFullscreen } = this.state;
|
|
|
|
const newIsFullscreen = FullscreenService.isFullScreen(this.screenshareContainer);
|
|
|
|
if (isFullscreen !== newIsFullscreen) {
|
|
|
|
this.setState({ isFullscreen: newIsFullscreen });
|
2021-02-24 01:04:00 +08:00
|
|
|
layoutContextDispatch({ type: 'setScreenShareFullscreen', value: newIsFullscreen });
|
2019-07-27 00:48:51 +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 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-08 02:12:28 +08:00
|
|
|
renderFullscreenButton() {
|
2019-07-27 00:48:51 +08:00
|
|
|
const { intl } = this.props;
|
|
|
|
const { isFullscreen } = this.state;
|
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}
|
|
|
|
isFullscreen={isFullscreen}
|
|
|
|
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) {
|
|
|
|
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%' }}
|
|
|
|
playsInline
|
|
|
|
onLoadedData={this.onLoadedData}
|
|
|
|
ref={(ref) => {
|
|
|
|
this.videoTag = ref;
|
|
|
|
}}
|
|
|
|
muted
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScreenshareContainerInside(mainText) {
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={styles.screenshareContainerInside}>
|
|
|
|
<h1 className={styles.mainText}>{mainText}</h1>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScreensharePresenter() {
|
|
|
|
const { loaded, switched } = this.state;
|
|
|
|
const { isGloballyBroadcasting, intl } = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={switched ? styles.screenshareContainer : styles.screenshareContainerPresenter}
|
|
|
|
key="screenshareContainer"
|
|
|
|
ref={(ref) => { this.screenshareContainer = ref; }}
|
|
|
|
>
|
|
|
|
{loaded && this.renderSwitchButton()}
|
|
|
|
{this.renderVideo(switched)}
|
|
|
|
|
|
|
|
{isGloballyBroadcasting
|
|
|
|
? (
|
|
|
|
<div>
|
|
|
|
{!switched
|
|
|
|
&& this.renderScreenshareContainerInside(intl.formatMessage(intlMessages.presenterSharingLabel))}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
: this.renderScreenshareContainerInside(intl.formatMessage(intlMessages.presenterLoadingLabel))
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderScreenshareDefault() {
|
|
|
|
const { isFullscreen } = this.props;
|
|
|
|
const { loaded } = this.state;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={styles.screenshareContainer}
|
|
|
|
key="screenshareContainer"
|
|
|
|
ref={(ref) => {
|
|
|
|
this.screenshareContainer = ref;
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{isFullscreen && <PollingContainer />}
|
|
|
|
{loaded && this.renderFullscreenButton()}
|
|
|
|
{this.renderVideo(true)}
|
|
|
|
</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-04-23 00:17:20 +08:00
|
|
|
const { isPresenter, isGloballyBroadcasting } = this.props;
|
2021-02-06 06:18:39 +08:00
|
|
|
|
|
|
|
// Conditions to render the (re)connecting spinner and the unhealthy stream
|
|
|
|
// 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)
|
|
|
|
|| !isStreamHealthy && loaded && isGloballyBroadcasting;
|
2019-01-08 02:12:28 +08:00
|
|
|
|
2017-07-25 03:29:34 +08:00
|
|
|
return (
|
2021-02-06 06:18:39 +08:00
|
|
|
[(shouldRenderConnectingState)
|
2019-07-24 03:56:39 +08:00
|
|
|
? (
|
|
|
|
<div
|
|
|
|
key={_.uniqueId('screenshareArea-')}
|
|
|
|
className={styles.connecting}
|
2020-10-27 23:54:55 +08:00
|
|
|
data-test="screenshareConnecting"
|
2019-07-24 03:56:39 +08:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
: null,
|
2019-08-03 05:32:42 +08:00
|
|
|
!autoplayBlocked
|
|
|
|
? null
|
2020-12-10 06:00:54 +08:00
|
|
|
: (this.renderAutoplayOverlay()),
|
2021-06-03 02:31:20 +08:00
|
|
|
isPresenter
|
|
|
|
? this.renderScreensharePresenter()
|
|
|
|
: this.renderScreenshareDefault(),
|
|
|
|
]
|
2017-07-25 03:29:34 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2019-02-07 05:12:59 +08:00
|
|
|
|
2021-02-24 01:04:00 +08:00
|
|
|
export default injectIntl(withLayoutConsumer(ScreenshareComponent));
|
2019-02-07 05:12:59 +08:00
|
|
|
|
|
|
|
ScreenshareComponent.propTypes = {
|
2020-05-26 04:00:13 +08:00
|
|
|
intl: PropTypes.object.isRequired,
|
2019-02-08 01:47:28 +08:00
|
|
|
isPresenter: PropTypes.bool.isRequired,
|
2019-02-07 05:12:59 +08:00
|
|
|
};
|