2019-08-02 08:28:14 +02:00
|
|
|
// @flow
|
2021-04-12 18:43:47 +02:00
|
|
|
import { ENABLE_PREROLL_ADS } from 'config';
|
|
|
|
import * as PAGES from 'constants/pages';
|
|
|
|
import * as ICONS from 'constants/icons';
|
2020-04-16 23:43:09 +02:00
|
|
|
import React, { useEffect, useState, useContext, useCallback } from 'react';
|
2019-08-02 08:28:14 +02:00
|
|
|
import { stopContextMenu } from 'util/context-menu';
|
2020-04-28 21:33:30 +02:00
|
|
|
import type { Player } from './internal/videojs';
|
2020-04-16 01:21:17 +02:00
|
|
|
import VideoJs from './internal/videojs';
|
2020-01-22 18:19:49 +01:00
|
|
|
import analytics from 'analytics';
|
2020-01-31 19:25:48 +01:00
|
|
|
import { EmbedContext } from 'page/embedWrapper/view';
|
2020-04-14 01:48:11 +02:00
|
|
|
import classnames from 'classnames';
|
2020-03-27 17:49:41 +01:00
|
|
|
import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
2020-04-14 01:48:11 +02:00
|
|
|
import AutoplayCountdown from 'component/autoplayCountdown';
|
|
|
|
import usePrevious from 'effects/use-previous';
|
2020-05-07 20:44:11 +02:00
|
|
|
import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
2020-04-29 22:50:06 +02:00
|
|
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
2020-04-16 23:43:09 +02:00
|
|
|
import LoadingScreen from 'component/common/loading-screen';
|
2021-01-08 16:21:27 +01:00
|
|
|
import { addTheaterModeButton } from './internal/theater-mode';
|
2021-04-12 18:43:47 +02:00
|
|
|
import { useGetAds } from 'effects/use-get-ads';
|
|
|
|
import Button from 'component/button';
|
|
|
|
import I18nMessage from 'component/i18nMessage';
|
|
|
|
import { useHistory } from 'react-router';
|
2019-08-02 08:28:14 +02:00
|
|
|
|
2020-04-28 21:33:30 +02:00
|
|
|
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
2021-01-27 14:49:30 +01:00
|
|
|
const PLAY_TIMEOUT_LIMIT = 2000;
|
2020-04-28 21:33:30 +02:00
|
|
|
|
2019-08-02 08:28:14 +02:00
|
|
|
type Props = {
|
2019-09-06 02:26:03 +02:00
|
|
|
position: number,
|
2021-02-17 10:28:20 +01:00
|
|
|
changeVolume: (number) => void,
|
|
|
|
changeMute: (boolean) => void,
|
2019-08-02 08:28:14 +02:00
|
|
|
source: string,
|
|
|
|
contentType: string,
|
2019-11-28 09:14:07 +01:00
|
|
|
thumbnail: string,
|
2020-08-07 22:59:20 +02:00
|
|
|
claim: StreamClaim,
|
2020-04-16 23:43:09 +02:00
|
|
|
muted: boolean,
|
2021-01-14 14:16:04 +01:00
|
|
|
videoPlaybackRate: number,
|
2020-04-16 23:43:09 +02:00
|
|
|
volume: number,
|
2020-04-14 01:48:11 +02:00
|
|
|
uri: string,
|
|
|
|
autoplaySetting: boolean,
|
|
|
|
autoplayIfEmbedded: boolean,
|
2020-05-05 20:02:12 +02:00
|
|
|
desktopPlayStartTime?: number,
|
2020-04-14 01:48:11 +02:00
|
|
|
doAnalyticsView: (string, number) => Promise<any>,
|
2020-08-07 22:59:20 +02:00
|
|
|
doAnalyticsBuffer: (string, any) => void,
|
2020-04-14 01:48:11 +02:00
|
|
|
claimRewards: () => void,
|
2020-04-30 09:49:52 +02:00
|
|
|
savePosition: (string, number) => void,
|
2021-02-17 10:28:20 +01:00
|
|
|
clearPosition: (string) => void,
|
2021-01-08 16:21:27 +01:00
|
|
|
toggleVideoTheaterMode: () => void,
|
2021-02-17 10:28:20 +01:00
|
|
|
setVideoPlaybackRate: (number) => void,
|
2021-04-12 18:43:47 +02:00
|
|
|
authenticated: boolean,
|
|
|
|
homepageData: {
|
|
|
|
PRIMARY_CONTENT_CHANNEL_IDS?: Array<string>,
|
|
|
|
ENLIGHTENMENT_CHANNEL_IDS?: Array<string>,
|
|
|
|
GAMING_CHANNEL_IDS?: Array<string>,
|
|
|
|
SCIENCE_CHANNEL_IDS?: Array<string>,
|
|
|
|
TECHNOLOGY_CHANNEL_IDS?: Array<string>,
|
|
|
|
COMMUNITY_CHANNEL_IDS?: Array<string>,
|
|
|
|
FINCANCE_CHANNEL_IDS?: Array<string>,
|
|
|
|
},
|
2019-08-02 08:28:14 +02:00
|
|
|
};
|
|
|
|
|
2020-04-16 01:21:17 +02:00
|
|
|
/*
|
|
|
|
codesandbox of idealized/clean videojs and react 16+
|
|
|
|
https://codesandbox.io/s/71z2lm4ko6
|
|
|
|
*/
|
|
|
|
|
2019-08-02 08:28:14 +02:00
|
|
|
function VideoViewer(props: Props) {
|
2020-02-04 22:14:08 +01:00
|
|
|
const {
|
|
|
|
contentType,
|
|
|
|
source,
|
|
|
|
changeVolume,
|
|
|
|
changeMute,
|
2021-01-14 14:16:04 +01:00
|
|
|
videoPlaybackRate,
|
2020-02-04 22:14:08 +01:00
|
|
|
thumbnail,
|
2020-03-19 21:25:37 +01:00
|
|
|
position,
|
2020-02-04 22:14:08 +01:00
|
|
|
claim,
|
2020-04-14 01:48:11 +02:00
|
|
|
uri,
|
2020-04-16 23:43:09 +02:00
|
|
|
muted,
|
|
|
|
volume,
|
2020-04-14 01:48:11 +02:00
|
|
|
autoplaySetting,
|
|
|
|
autoplayIfEmbedded,
|
|
|
|
doAnalyticsView,
|
2020-08-07 22:59:20 +02:00
|
|
|
doAnalyticsBuffer,
|
2020-04-14 01:48:11 +02:00
|
|
|
claimRewards,
|
2020-04-30 09:49:52 +02:00
|
|
|
savePosition,
|
2020-05-15 03:18:54 +02:00
|
|
|
clearPosition,
|
2020-05-05 20:02:12 +02:00
|
|
|
desktopPlayStartTime,
|
2021-01-08 16:21:27 +01:00
|
|
|
toggleVideoTheaterMode,
|
2021-01-14 14:16:04 +01:00
|
|
|
setVideoPlaybackRate,
|
2021-04-12 18:43:47 +02:00
|
|
|
homepageData,
|
|
|
|
authenticated,
|
2020-02-04 22:14:08 +01:00
|
|
|
} = props;
|
2021-04-12 18:43:47 +02:00
|
|
|
const {
|
|
|
|
PRIMARY_CONTENT_CHANNEL_IDS = [],
|
|
|
|
ENLIGHTENMENT_CHANNEL_IDS = [],
|
|
|
|
GAMING_CHANNEL_IDS = [],
|
|
|
|
SCIENCE_CHANNEL_IDS = [],
|
|
|
|
TECHNOLOGY_CHANNEL_IDS = [],
|
|
|
|
COMMUNITY_CHANNEL_IDS = [],
|
|
|
|
FINCANCE_CHANNEL_IDS = [],
|
|
|
|
} = homepageData;
|
|
|
|
const adApprovedChannelIds = [
|
|
|
|
...PRIMARY_CONTENT_CHANNEL_IDS,
|
|
|
|
...ENLIGHTENMENT_CHANNEL_IDS,
|
|
|
|
...GAMING_CHANNEL_IDS,
|
|
|
|
...SCIENCE_CHANNEL_IDS,
|
|
|
|
...TECHNOLOGY_CHANNEL_IDS,
|
|
|
|
...COMMUNITY_CHANNEL_IDS,
|
|
|
|
...FINCANCE_CHANNEL_IDS,
|
|
|
|
];
|
2020-01-22 18:19:49 +01:00
|
|
|
const claimId = claim && claim.claim_id;
|
2021-04-12 18:43:47 +02:00
|
|
|
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
2019-11-28 09:14:07 +01:00
|
|
|
const isAudio = contentType.includes('audio');
|
2020-03-27 17:49:41 +01:00
|
|
|
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
2021-04-12 18:43:47 +02:00
|
|
|
const {
|
|
|
|
location: { pathname },
|
|
|
|
} = useHistory();
|
2020-04-14 01:48:11 +02:00
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
|
|
|
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
2021-02-17 18:01:12 +01:00
|
|
|
const vjsCallbackDataRef: any = React.useRef();
|
2021-04-12 18:43:47 +02:00
|
|
|
const previousUri = usePrevious(uri);
|
|
|
|
const embedded = useContext(EmbedContext);
|
|
|
|
const approvedVideo = Boolean(channelClaimId) && adApprovedChannelIds.includes(channelClaimId);
|
|
|
|
const adsEnabled = ENABLE_PREROLL_ADS && !authenticated && !embedded && approvedVideo;
|
2021-04-26 22:28:25 +02:00
|
|
|
const [adUrl, setAdUrl, isFetchingAd] = useGetAds(approvedVideo, adsEnabled);
|
2020-04-26 22:32:39 +02:00
|
|
|
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
|
|
|
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
2020-04-16 23:43:09 +02:00
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2019-10-08 10:06:28 +02:00
|
|
|
|
2020-04-16 23:43:09 +02:00
|
|
|
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
|
|
|
|
useEffect(() => {
|
|
|
|
if (uri && previousUri && uri !== previousUri) {
|
|
|
|
setShowAutoplayCountdown(false);
|
|
|
|
setIsEndededEmbed(false);
|
|
|
|
setIsLoading(false);
|
|
|
|
}
|
|
|
|
}, [uri, previousUri]);
|
|
|
|
|
2021-02-17 18:01:12 +01:00
|
|
|
// Update vjsCallbackDataRef (ensures videojs callbacks are not using stale values):
|
|
|
|
useEffect(() => {
|
|
|
|
vjsCallbackDataRef.current = {
|
|
|
|
embedded: embedded,
|
|
|
|
videoPlaybackRate: videoPlaybackRate,
|
|
|
|
};
|
2021-03-09 03:55:55 +01:00
|
|
|
}, [embedded, videoPlaybackRate]);
|
2021-02-17 18:01:12 +01:00
|
|
|
|
2020-04-16 01:21:17 +02:00
|
|
|
function doTrackingBuffered(e: Event, data: any) {
|
2021-02-17 10:28:20 +01:00
|
|
|
fetch(source, { method: 'HEAD' }).then((response) => {
|
2020-09-09 20:54:51 +02:00
|
|
|
data.playerPoweredBy = response.headers.get('x-powered-by');
|
|
|
|
doAnalyticsBuffer(uri, data);
|
|
|
|
});
|
2020-04-16 01:21:17 +02:00
|
|
|
}
|
2019-08-13 07:35:13 +02:00
|
|
|
|
2020-04-16 01:21:17 +02:00
|
|
|
function doTrackingFirstPlay(e: Event, data: any) {
|
2020-05-05 20:02:12 +02:00
|
|
|
let timeToStartInMs = data.secondsToLoad * 1000;
|
2020-05-05 22:45:59 +02:00
|
|
|
|
2020-05-05 20:02:12 +02:00
|
|
|
if (desktopPlayStartTime !== undefined) {
|
|
|
|
const differenceToAdd = Date.now() - desktopPlayStartTime;
|
|
|
|
timeToStartInMs += differenceToAdd;
|
|
|
|
}
|
2020-11-26 22:06:58 +01:00
|
|
|
analytics.playerStartedEvent(embedded);
|
2020-05-05 16:09:53 +02:00
|
|
|
analytics.videoStartEvent(claimId, timeToStartInMs);
|
|
|
|
doAnalyticsView(uri, timeToStartInMs).then(() => {
|
2020-04-16 01:21:17 +02:00
|
|
|
claimRewards();
|
|
|
|
});
|
|
|
|
}
|
2020-04-14 01:48:11 +02:00
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
const onEnded = React.useCallback(() => {
|
|
|
|
if (adUrl) {
|
|
|
|
setAdUrl(null);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-16 01:21:17 +02:00
|
|
|
if (embedded) {
|
|
|
|
setIsEndededEmbed(true);
|
|
|
|
} else if (autoplaySetting) {
|
|
|
|
setShowAutoplayCountdown(true);
|
2020-01-22 18:19:49 +01:00
|
|
|
}
|
2021-04-12 18:43:47 +02:00
|
|
|
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl]);
|
2020-04-08 19:10:33 +02:00
|
|
|
|
2020-04-16 01:21:17 +02:00
|
|
|
function onPlay() {
|
2020-04-16 23:43:09 +02:00
|
|
|
setIsLoading(false);
|
2020-04-16 01:21:17 +02:00
|
|
|
setIsPlaying(true);
|
|
|
|
setShowAutoplayCountdown(false);
|
|
|
|
setIsEndededEmbed(false);
|
|
|
|
}
|
2020-04-08 19:10:33 +02:00
|
|
|
|
2020-05-15 03:18:54 +02:00
|
|
|
function handlePosition(player) {
|
|
|
|
if (player.ended()) {
|
|
|
|
clearPosition(uri);
|
|
|
|
} else {
|
|
|
|
savePosition(uri, player.currentTime());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 03:55:55 +01:00
|
|
|
function restorePlaybackRate(player) {
|
2021-02-17 18:01:12 +01:00
|
|
|
if (!vjsCallbackDataRef.current.embedded) {
|
|
|
|
player.playbackRate(vjsCallbackDataRef.current.videoPlaybackRate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
const playerReadyDependencyList = [uri, adUrl, embedded, autoplayIfEmbedded];
|
|
|
|
if (!IS_WEB) {
|
|
|
|
playerReadyDependencyList.push(desktopPlayStartTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
const onPlayerReady = useCallback((player: Player) => {
|
|
|
|
if (!embedded) {
|
|
|
|
player.muted(muted);
|
|
|
|
player.volume(volume);
|
|
|
|
player.playbackRate(videoPlaybackRate);
|
|
|
|
addTheaterModeButton(player, toggleVideoTheaterMode);
|
|
|
|
}
|
2020-04-27 21:23:07 +02:00
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
const shouldPlay = !embedded || autoplayIfEmbedded;
|
|
|
|
// https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
|
|
|
|
if (shouldPlay) {
|
|
|
|
const playPromise = player.play();
|
|
|
|
const timeoutPromise = new Promise((resolve, reject) =>
|
|
|
|
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
|
|
|
|
);
|
2020-04-28 19:25:13 +02:00
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
2021-05-19 17:56:54 +02:00
|
|
|
if (typeof error === 'object' && error.name && error.name === 'NotAllowedError') {
|
|
|
|
if (player.autoplay() && !player.muted()) {
|
|
|
|
player.muted(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
if (PLAY_TIMEOUT_ERROR) {
|
|
|
|
const retryPlayPromise = player.play();
|
|
|
|
Promise.race([retryPlayPromise, timeoutPromise]).catch((error) => {
|
2020-04-29 16:36:43 +02:00
|
|
|
setIsLoading(false);
|
|
|
|
setIsPlaying(false);
|
2021-04-12 18:43:47 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setIsLoading(false);
|
|
|
|
setIsPlaying(false);
|
2021-01-14 14:16:04 +01:00
|
|
|
}
|
|
|
|
});
|
2021-04-12 18:43:47 +02:00
|
|
|
}
|
2020-04-24 22:40:31 +02:00
|
|
|
|
2021-04-12 18:43:47 +02:00
|
|
|
setIsLoading(shouldPlay); // if we are here outside of an embed, we're playing
|
|
|
|
|
|
|
|
// PR: #5535
|
|
|
|
// Move the restoration to a later `loadedmetadata` phase to counter the
|
|
|
|
// delay from the header-fetch. This is a temp change until the next
|
|
|
|
// re-factoring.
|
|
|
|
player.on('loadedmetadata', () => restorePlaybackRate(player));
|
|
|
|
|
|
|
|
player.on('tracking:buffered', doTrackingBuffered);
|
|
|
|
player.on('tracking:firstplay', doTrackingFirstPlay);
|
|
|
|
player.on('ended', onEnded);
|
|
|
|
player.on('play', onPlay);
|
|
|
|
player.on('pause', () => {
|
|
|
|
setIsPlaying(false);
|
|
|
|
handlePosition(player);
|
|
|
|
});
|
|
|
|
player.on('error', () => {
|
|
|
|
const error = player.error();
|
|
|
|
if (error) {
|
|
|
|
analytics.sentryError('Video.js error', error);
|
2020-04-16 23:43:09 +02:00
|
|
|
}
|
2021-04-12 18:43:47 +02:00
|
|
|
});
|
|
|
|
player.on('volumechange', () => {
|
|
|
|
if (player) {
|
|
|
|
changeVolume(player.volume());
|
|
|
|
changeMute(player.muted());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
player.on('ratechange', () => {
|
|
|
|
const HAVE_NOTHING = 0; // https://docs.videojs.com/player#readyState
|
|
|
|
if (player && player.readyState() !== HAVE_NOTHING) {
|
|
|
|
// The playbackRate occasionally resets to 1, typically when loading a fresh video or when 'src' changes.
|
|
|
|
// Videojs says it's a browser quirk (https://github.com/videojs/video.js/issues/2516).
|
|
|
|
// [x] Don't update 'videoPlaybackRate' in this scenario.
|
|
|
|
// [ ] Ideally, the controlBar should be hidden to prevent users from changing the rate while loading.
|
|
|
|
setVideoPlaybackRate(player.playbackRate());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (position) {
|
|
|
|
player.currentTime(position);
|
|
|
|
}
|
|
|
|
player.on('dispose', () => {
|
|
|
|
handlePosition(player);
|
|
|
|
});
|
|
|
|
}, playerReadyDependencyList);
|
2020-03-19 21:25:37 +01:00
|
|
|
|
2019-08-02 08:28:14 +02:00
|
|
|
return (
|
2020-04-14 01:48:11 +02:00
|
|
|
<div
|
|
|
|
className={classnames('file-viewer', {
|
|
|
|
'file-viewer--is-playing': isPlaying,
|
2020-04-16 23:43:09 +02:00
|
|
|
'file-viewer--ended-embed': isEndededEmbed,
|
2020-04-14 01:48:11 +02:00
|
|
|
})}
|
|
|
|
onContextMenu={stopContextMenu}
|
|
|
|
>
|
|
|
|
{showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
|
|
|
|
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
|
|
|
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
2020-04-26 22:32:39 +02:00
|
|
|
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
2020-04-28 19:25:13 +02:00
|
|
|
{isLoading && <LoadingScreen status={__('Loading')} />}
|
2021-04-12 18:43:47 +02:00
|
|
|
|
|
|
|
{!isFetchingAd && adUrl && (
|
|
|
|
<>
|
|
|
|
<span className="ads__video-notify">
|
|
|
|
{__('Advertisement')}{' '}
|
|
|
|
<Button
|
|
|
|
className="ads__video-close"
|
|
|
|
icon={ICONS.REMOVE}
|
|
|
|
title={__('Close')}
|
|
|
|
onClick={() => setAdUrl(null)}
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
<span className="ads__video-nudge">
|
|
|
|
<I18nMessage
|
|
|
|
tokens={{
|
|
|
|
sign_up: (
|
|
|
|
<Button
|
|
|
|
button="secondary"
|
|
|
|
className="ads__video-link"
|
|
|
|
label={__('Sign Up')}
|
|
|
|
navigate={`/$/${PAGES.AUTH}?redirect=${pathname}&src=video-ad`}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
%sign_up% to turn ads off.
|
|
|
|
</I18nMessage>
|
|
|
|
</span>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{!isFetchingAd && (
|
|
|
|
<VideoJs
|
|
|
|
adUrl={adUrl}
|
|
|
|
source={adUrl || source}
|
|
|
|
sourceType={forcePlayer || adUrl ? 'video/mp4' : contentType}
|
|
|
|
isAudio={isAudio}
|
|
|
|
poster={isAudio || (embedded && !autoplayIfEmbedded) ? thumbnail : ''}
|
|
|
|
onPlayerReady={onPlayerReady}
|
|
|
|
startMuted={autoplayIfEmbedded}
|
|
|
|
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
|
|
|
autoplay={!embedded || autoplayIfEmbedded}
|
|
|
|
/>
|
|
|
|
)}
|
2019-08-02 08:28:14 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default VideoViewer;
|