diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx index af0f39a0e9..39131535f1 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/component.jsx @@ -1,15 +1,23 @@ import React, { Component } from 'react'; import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component'; +import { defineMessages, injectIntl } from 'react-intl'; import ReactPlayer from 'react-player'; import { sendMessage, onMessage, removeAllListeners } from './service'; import logger from '/imports/startup/client/logger'; import ArcPlayer from './custom-players/arc-player'; - import { styles } from './styles'; +const intlMessages = defineMessages({ + autoPlayWarning: { + id: 'app.externalVideo.autoPlayWarning', + description: 'Shown when user needs to interact with player to make it work', + }, +}); + const SYNC_INTERVAL_SECONDS = 2; +const AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS = 5; ReactPlayer.addCustomPlayer(ArcPlayer); @@ -21,14 +29,26 @@ class VideoPlayer extends Component { this.player = null; this.syncInterval = null; + this.autoPlayTimeout = null; this.state = { mutedByEchoTest: false, playing: false, + hasPlayedBefore: false, + autoPlayBlocked: false, playbackRate: 1, }; this.opts = { - controls: isPresenter, + file: { + attributes: { + controls: true, + }, + }, + dailymotion: { + params: { + controls: true, + }, + }, youtube: { playerVars: { autoplay: 1, @@ -43,7 +63,9 @@ class VideoPlayer extends Component { }; this.registerVideoListeners = this.registerVideoListeners.bind(this); + this.autoPlayBlockDetected = this.autoPlayBlockDetected.bind(this); this.clearVideoListeners = this.clearVideoListeners.bind(this); + this.handleFirstPlay = this.handleFirstPlay.bind(this); this.handleResize = this.handleResize.bind(this); this.handleOnReady = this.handleOnReady.bind(this); this.handleOnPlay = this.handleOnPlay.bind(this); @@ -65,6 +87,7 @@ class VideoPlayer extends Component { this.clearVideoListeners(); clearInterval(this.syncInterval); + clearTimeout(this.autoPlayTimeout); this.player = null; } @@ -83,6 +106,24 @@ class VideoPlayer extends Component { return { mutedByEchoTest: inEchoTest }; } + autoPlayBlockDetected() { + this.setState({autoPlayBlocked: true}); + } + + handleFirstPlay() { + const { isPresenter } = this.props; + const { hasPlayedBefore } = this.state; + + if (!hasPlayedBefore) { + this.setState({ hasPlayedBefore: true, autoPlayBlocked: false }); + clearTimeout(this.autoPlayTimeout); + + if (isPresenter) { + sendMessage('presenterReady'); + } + } + } + getCurrentPlaybackRate() { const intPlayer = this.player.getInternalPlayer(); @@ -124,14 +165,29 @@ class VideoPlayer extends Component { if (isPresenter) { this.syncInterval = setInterval(() => { + const { playing, hasPlayedBefore } = this.state; const curTime = this.player.getCurrentTime(); const rate = this.getCurrentPlaybackRate(); - sendMessage('playerUpdate', { rate, time: curTime, state: this.state.playing }); + // Always pause video if presenter is has not started sharing, e.g., blocked by autoplay + const playingState = hasPlayedBefore ? playing : false; + + sendMessage('playerUpdate', { rate, time: curTime, state: playingState }); }, SYNC_INTERVAL_SECONDS * 1000); + + onMessage('viewerJoined', () => { + const { hasPlayedBefore } = this.state; + + logger.debug({ logCode: 'external_video_viewer_joined' }, 'Viewer joined external video'); + if (hasPlayedBefore) { + sendMessage('presenterReady'); + } + }); } else { onMessage('play', ({ time }) => { - if (!this.player) { + const { hasPlayedBefore } = this.state; + + if (!this.player || !hasPlayedBefore) { return; } @@ -142,7 +198,9 @@ class VideoPlayer extends Component { }); onMessage('stop', ({ time }) => { - if (!this.player) { + const { hasPlayedBefore } = this.state; + + if (!this.player || !hasPlayedBefore) { return; } this.player.seekTo(time); @@ -151,8 +209,20 @@ class VideoPlayer extends Component { logger.debug({ logCode: 'external_video_client_stop' }, 'Stop external video'); }); + onMessage('presenterReady', (data) => { + const { hasPlayedBefore } = this.state; + + logger.debug({ logCode: 'external_video_presenter_ready' }, 'Presenter is ready to sync'); + + if (!hasPlayedBefore) { + this.setState({playing: true}); + } + }); + onMessage('playerUpdate', (data) => { - if (!this.player) { + const { hasPlayedBefore, playing } = this.state; + + if (!this.player || !hasPlayedBefore) { return; } @@ -176,7 +246,7 @@ class VideoPlayer extends Component { }, 'Seek external video to:'); } - if (this.state.playing !== data.state) { + if (playing !== data.state) { this.setState({ playing: data.state }); } }); @@ -185,12 +255,21 @@ class VideoPlayer extends Component { handleOnReady() { const { isPresenter } = this.props; + const { hasPlayedBefore } = this.state; + + if (hasPlayedBefore) { + return; + } if (!isPresenter) { sendMessage('viewerJoined'); + } else { + this.setState({ playing: true }); } this.handleResize(); + + this.autoPlayTimeout = setTimeout(this.autoPlayBlockDetected, AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000); } handleOnPlay() { @@ -201,6 +280,8 @@ class VideoPlayer extends Component { sendMessage('play', { time: curTime }); } this.setState({ playing: true }); + + this.handleFirstPlay(); } handleOnPause() { @@ -211,11 +292,13 @@ class VideoPlayer extends Component { sendMessage('stop', { time: curTime }); } this.setState({ playing: false }); + + this.handleFirstPlay(); } render() { - const { videoUrl } = this.props; - const { playing, playbackRate, mutedByEchoTest } = this.state; + const { videoUrl, intl } = this.props; + const { playing, playbackRate, mutedByEchoTest, autoPlayBlocked } = this.state; return (
{ this.playerParent = ref; }} > + {autoPlayBlocked ? +

+ {intl.formatMessage(intlMessages.autoPlayWarning)} +

+ : '' + } ( ); -export default injectIntl(withTracker(({ intl, isPresenter }) => { - const title = intl.formatMessage(intlMessages.title); +export default withTracker(({ isPresenter }) => { const inEchoTest = Session.get('inEchoTest'); return { inEchoTest, - title, isPresenter, videoUrl: getVideoUrl(), }; -})(ExternalVideoContainer)); +})(ExternalVideoContainer); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.scss b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.scss index e4d8dbe273..2ca1d27d66 100755 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/modal/styles.scss @@ -13,7 +13,7 @@ display: flex; flex-direction: column; justify-content: center; - padding-bottom: 0; + padding: 0; margin-right: auto; margin-left: auto; width: 100%; @@ -32,7 +32,7 @@ .modal { @extend .modal; padding: 1.5rem; - min-height: 20rem; + min-height: 23rem; } .closeBtn { @@ -132,8 +132,8 @@ .urlError { color: red; - padding: 1em; - + padding: 1em 0 2.5em 0; + :global(.animationsEnabled) & { transition: 1s; } @@ -143,5 +143,5 @@ color: var(--color-gray); font-size: var(--font-size-small); font-style: italic; - padding-top: var(--sm-padding-x); + padding-top: var(--sm-padding-y); } diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/styles.scss b/bigbluebutton-html5/imports/ui/components/external-video-player/styles.scss index 2af8c737bd..1b888e2bb3 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/styles.scss @@ -12,3 +12,17 @@ border-style: none; border-bottom: none; } + +.autoPlayWarning { + position: absolute; + z-index: 100; + font-size: x-large; + color: white; + width: 100%; + background-color: rgba(6,23,42,0.5); + bottom: 20%; + vertical-align: middle; + text-align: center; + pointer-events: none; + +} diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 7a61843174..daa33dbe42 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -658,9 +658,10 @@ "app.externalVideo.urlInput": "Add Video URL", "app.externalVideo.urlError": "This video URL isn't supported", "app.externalVideo.close": "Close", + "app.externalVideo.autoPlayWarning": "Play the video to enable media synchronization", "app.network.connection.effective.slow": "We're noticing connectivity issues.", "app.network.connection.effective.slow.help": "More information", - "app.externalVideo.noteLabel": "Note: Shared external videos will not appear in the recording", + "app.externalVideo.noteLabel": "Note: Shared external videos will not appear in the recording. YouTube, Vimeo, Instructure Media, Twitch and Daily Motion URLs are supported.", "app.actionsBar.actionsDropdown.shareExternalVideo": "Share an external video", "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing external video", "app.iOSWarning.label": "Please upgrade to iOS 12.2 or higher",