import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import PropTypes from 'prop-types'; import _ from 'lodash'; import FullscreenButtonContainer from '../fullscreen-button/container'; import SwitchButtonContainer from './switch-button/container'; import VolumeSlider from '../external-video-player/volume-slider/component'; import { styles } from './styles'; import AutoplayOverlay from '../media/autoplay-overlay/component'; import logger from '/imports/startup/client/logger'; import playAndRetry from '/imports/utils/mediaElementPlayRetry'; import { notify } from '/imports/ui/services/notification'; import { SCREENSHARE_MEDIA_ELEMENT_NAME, screenshareHasEnded, screenshareHasStarted, getMediaElement, attachLocalPreviewStream, setVolume, getVolume, } from '/imports/ui/components/screenshare/service'; import { isStreamStateUnhealthy, subscribeToStreamStateChange, unsubscribeFromStreamStateChange, } from '/imports/ui/services/bbb-webrtc-sfu/stream-state-service'; import { ACTIONS } from '/imports/ui/components/layout/enums'; import deviceInfo from '/imports/utils/deviceInfo'; import Settings from '/imports/ui/services/settings'; const intlMessages = defineMessages({ screenShareLabel: { id: 'app.screenshare.screenShareLabel', description: 'screen share area element label', }, presenterLoadingLabel: { id: 'app.screenshare.presenterLoadingLabel', }, viewerLoadingLabel: { id: 'app.screenshare.viewerLoadingLabel', }, presenterSharingLabel: { id: 'app.screenshare.presenterSharingLabel', }, autoplayBlockedDesc: { id: 'app.media.screenshare.autoplayBlockedDesc', }, autoplayAllowLabel: { id: 'app.media.screenshare.autoplayAllowLabel', }, 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', }, screenshareEndedDueToDataSaving: { id: 'app.media.screenshare.endDueToDataSaving', description: 'toast to show when a screenshare has ended by changing data savings option', }, }); const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen; const MOBILE_HOVER_TIMEOUT = 5000; class ScreenshareComponent extends React.Component { static renderScreenshareContainerInside(mainText) { return (

{mainText}

); } constructor() { super(); this.state = { restoreOnUnmount: true, loaded: false, autoplayBlocked: false, isStreamHealthy: false, switched: false, // Volume control hover toolbar showHoverToolBar: false, }; this.onLoadedData = this.onLoadedData.bind(this); this.handleAllowAutoplay = this.handleAllowAutoplay.bind(this); this.handlePlayElementFailed = this.handlePlayElementFailed.bind(this); this.failedMediaElements = []; this.onStreamStateChange = this.onStreamStateChange.bind(this); this.onSwitched = this.onSwitched.bind(this); this.handleOnVolumeChanged = this.handleOnVolumeChanged.bind(this); this.handleOnMuted = this.handleOnMuted.bind(this); this.volume = getVolume(); this.mobileHoverSetTimeout = null; } componentDidMount() { const { getSwapLayout, toggleSwapLayout, layoutContextDispatch, intl, hidePresentation, } = this.props; screenshareHasStarted(); // Autoplay failure handling window.addEventListener('screensharePlayFailed', this.handlePlayElementFailed); // 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()); notify(intl.formatMessage(intlMessages.screenshareStarted), 'info', 'desktop'); if (getSwapLayout()) { toggleSwapLayout(layoutContextDispatch) this.setState({ restoreOnUnmount: false }); }; if (hidePresentation) { layoutContextDispatch({ type: ACTIONS.SET_PRESENTATION_IS_OPEN, value: true, }); } } componentDidUpdate(prevProps) { const { isPresenter, } = this.props; if (prevProps.isPresenter && !isPresenter) { screenshareHasEnded(); } } componentWillUnmount() { const { intl, fullscreenContext, layoutContextDispatch, hidePresentation, toggleSwapLayout, } = this.props; const { restoreOnUnmount } = this.state; screenshareHasEnded(); window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed); unsubscribeFromStreamStateChange('screenshare', this.onStreamStateChange); if (!Settings.dataSaving.viewScreenshare) { notify(intl.formatMessage(intlMessages.screenshareEndedDueToDataSaving), 'info', 'desktop'); } else { notify(intl.formatMessage(intlMessages.screenshareEnded), 'info', 'desktop'); } if (fullscreenContext) { layoutContextDispatch({ type: ACTIONS.SET_FULLSCREEN_ELEMENT, value: { element: '', group: '', }, }); } if (hidePresentation || !restoreOnUnmount) { layoutContextDispatch({ type: ACTIONS.SET_PRESENTATION_IS_OPEN, value: false, }); toggleSwapLayout(layoutContextDispatch); } } handleAllowAutoplay() { const { autoplayBlocked } = this.state; logger.info({ logCode: 'screenshare_autoplay_allowed', }, 'Screenshare media autoplay allowed by the user'); window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed); while (this.failedMediaElements.length) { const mediaElement = this.failedMediaElements.shift(); if (mediaElement) { 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'); } } } if (autoplayBlocked) { this.setState({ autoplayBlocked: false }); } } handlePlayElementFailed(e) { const { mediaElement } = e.detail; const { autoplayBlocked } = this.state; e.stopPropagation(); this.failedMediaElements.push(mediaElement); if (!autoplayBlocked) { logger.info({ logCode: 'screenshare_autoplay_prompt', }, 'Prompting user for action to play screenshare media'); this.setState({ autoplayBlocked: true }); } } 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 })); } handleOnVolumeChanged(volume) { this.volume = volume; setVolume(volume); } handleOnMuted(muted) { if (muted) { setVolume(0); } else { setVolume(this.volume); } } renderFullscreenButton() { const { intl, fullscreenElementId, fullscreenContext } = this.props; if (!ALLOW_FULLSCREEN) return null; return ( ); } renderAutoplayOverlay() { const { intl } = this.props; return ( ); } renderSwitchButton() { const { switched } = this.state; return ( ); } renderMobileVolumeControlOverlay () { return ( { this.overlay = ref; }} onTouchStart={() => { clearTimeout(this.mobileHoverSetTimeout); this.setState({ showHoverToolBar: true }); }} onTouchEnd={() => { this.mobileHoverSetTimeout = setTimeout( () => this.setState({ showHoverToolBar: false }), MOBILE_HOVER_TIMEOUT, ); }} /> ); } renderVolumeSlider() { const { showHoverToolBar } = this.state; const mobileHoverToolBarStyle = showHoverToolBar ? styles.showMobileHoverToolbar : styles.dontShowMobileHoverToolbar; const desktopHoverToolBarStyle = styles.hoverToolbar; const hoverToolbarStyle = deviceInfo.isMobile ? mobileHoverToolBarStyle : desktopHoverToolBarStyle; return [(
), (deviceInfo.isMobile) && this.renderMobileVolumeControlOverlay(), ]; } renderVideo(switched) { const { isGloballyBroadcasting } = this.props; return (