Merge pull request #8192 from lfzawacki/detect-autoplay-problems-external-video

Don't run synchronization code on a player with autoplay blocked and add an autoplay warning
This commit is contained in:
Chad Pilkey 2019-10-21 18:22:57 -04:00 committed by GitHub
commit 692eec9a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 28 deletions

View File

@ -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 (
<div
@ -223,6 +306,12 @@ class VideoPlayer extends Component {
data-test="videoPlayer"
ref={(ref) => { this.playerParent = ref; }}
>
{autoPlayBlocked ?
<p className={styles.autoPlayWarning}>
{intl.formatMessage(intlMessages.autoPlayWarning)}
</p>
: ''
}
<ReactPlayer
className={styles.videoPlayer}
url={videoUrl}
@ -240,4 +329,4 @@ class VideoPlayer extends Component {
}
}
export default injectWbResizeEvent(VideoPlayer);
export default injectIntl(injectWbResizeEvent(VideoPlayer));

View File

@ -1,28 +1,18 @@
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { withTracker } from 'meteor/react-meteor-data';
import { Session } from 'meteor/session';
import { getVideoUrl } from './service';
import ExternalVideo from './component';
const intlMessages = defineMessages({
title: {
id: 'app.externalVideo.title',
description: '',
},
});
const ExternalVideoContainer = props => (
<ExternalVideo {...{ ...props }} />
);
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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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",