lbry-desktop/ui/component/viewers/videoViewer/internal/videojs.jsx

379 lines
11 KiB
React
Raw Normal View History

2020-04-16 23:43:09 +02:00
// @flow
import 'videojs-contrib-ads'; // must be loaded in this order
import 'videojs-ima'; // loads directly after contrib-ads
import 'video.js/dist/alt/video-js-cdn.min.css';
import './plugins/videojs-mobile-ui/plugin';
import '@silvermine/videojs-chromecast/dist/silvermine-videojs-chromecast.css';
import * as ICONS from 'constants/icons';
import * as OVERLAY from './overlays';
import Button from 'component/button';
import classnames from 'classnames';
import events from './videojs-events';
import eventTracking from 'videojs-event-tracking';
import functions from './videojs-functions';
import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin';
import keyboardShorcuts from './videojs-keyboard-shortcuts';
import LbryVolumeBarClass from './lbry-volume-bar';
import Chromecast from './chromecast';
import playerjs from 'player.js';
import qualityLevels from 'videojs-contrib-quality-levels';
import React, { useEffect, useRef, useState } from 'react';
import recsys from './plugins/videojs-recsys/plugin';
2021-12-15 19:59:02 +01:00
// import runAds from './ads';
import videojs from 'video.js';
const canAutoplay = require('./plugins/canAutoplay');
2020-04-16 01:21:17 +02:00
require('@silvermine/videojs-chromecast')(videojs);
2020-04-28 21:33:30 +02:00
export type Player = {
controlBar: { addChild: (string, any) => void },
loadingSpinner: any,
autoplay: (any) => boolean,
chromecast: (any) => void,
currentTime: (?number) => number,
dispose: () => void,
ended: () => boolean,
2020-05-21 17:38:28 +02:00
error: () => any,
exitFullscreen: () => boolean,
getChild: (string) => any,
isFullscreen: () => boolean,
mobileUi: (any) => void,
muted: (?boolean) => boolean,
on: (string, (any) => void) => void,
one: (string, (any) => void) => void,
overlay: (any) => void,
play: () => Promise<any>,
playbackRate: (?number) => number,
readyState: () => number,
requestFullscreen: () => boolean,
userActive: (?boolean) => boolean,
volume: (?number) => number,
2020-04-28 21:33:30 +02:00
};
2020-04-16 01:21:17 +02:00
type Props = {
adUrl: ?string,
allowPreRoll: ?boolean,
autoplay: boolean,
autoplaySetting: boolean,
claimId: ?string,
title: ?string,
channelName: ?string,
embedded: boolean,
internalFeatureEnabled: ?boolean,
isAudio: boolean,
poster: ?string,
replay: boolean,
shareTelemetry: boolean,
source: string,
sourceType: string,
startMuted: boolean,
userId: ?number,
videoTheaterMode: boolean,
onPlayerReady: (Player, any) => void,
playNext: () => void,
playPrevious: () => void,
toggleVideoTheaterMode: () => void,
claimRewards: () => void,
doAnalyticsView: (string, number) => void,
uri: string,
claimValues: any,
clearPosition: (string) => void,
centerPlayButton: () => void,
2020-04-16 01:21:17 +02:00
};
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
2020-05-25 16:36:17 +02:00
const IS_IOS =
(/iPad|iPhone|iPod/.test(navigator.platform) ||
// for iOS 13+ , platform is MacIntel, so use this to test
2020-05-25 16:36:17 +02:00
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
!window.MSStream;
2020-04-16 01:21:17 +02:00
if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) {
videojs.registerPlugin('eventTracking', eventTracking);
}
if (!Object.keys(videojs.getPlugins()).includes('hlsQualitySelector')) {
videojs.registerPlugin('hlsQualitySelector', hlsQualitySelector);
}
if (!Object.keys(videojs.getPlugins()).includes('qualityLevels')) {
videojs.registerPlugin('qualityLevels', qualityLevels);
}
if (!Object.keys(videojs.getPlugins()).includes('recsys')) {
videojs.registerPlugin('recsys', recsys);
}
// ****************************************************************************
// VideoJs
// ****************************************************************************
2020-04-16 01:21:17 +02:00
/*
properties for this component should be kept to ONLY those that if changed should REQUIRE an entirely new videojs element
*/
2020-04-28 21:33:30 +02:00
export default React.memo<Props>(function VideoJs(props: Props) {
2021-04-12 18:43:47 +02:00
const {
// adUrl, // TODO: this ad functionality isn't used, can be pulled out
2021-12-15 19:59:02 +01:00
// allowPreRoll,
2021-04-12 18:43:47 +02:00
autoplay,
autoplaySetting,
claimId,
title,
channelName,
embedded,
2021-12-15 19:59:02 +01:00
// internalFeatureEnabled, // for people on the team to test new features internally
isAudio,
poster,
replay,
shareTelemetry,
2021-04-12 18:43:47 +02:00
source,
sourceType,
startMuted,
userId,
videoTheaterMode,
onPlayerReady,
playNext,
playPrevious,
toggleVideoTheaterMode,
claimValues,
doAnalyticsView,
claimRewards,
uri,
clearPosition,
centerPlayButton,
2021-04-12 18:43:47 +02:00
} = props;
// will later store the videojs player
const playerRef = useRef();
2020-04-16 23:43:09 +02:00
const containerRef = useRef();
const tapToUnmuteRef = useRef();
const tapToRetryRef = useRef();
const playerServerRef = useRef();
// initiate keyboard shortcuts
const { curried_function } = keyboardShorcuts({ toggleVideoTheaterMode, playNext, playPrevious });
const [reload, setReload] = useState('initial');
const { createVideoPlayerDOM } = functions({ isAudio });
const { unmuteAndHideHint, retryVideoAfterFailure, initializeEvents } = events({
tapToUnmuteRef,
tapToRetryRef,
setReload,
videoTheaterMode,
playerRef,
autoplaySetting,
replay,
claimValues,
userId,
claimId,
embedded,
doAnalyticsView,
claimRewards,
uri,
playerServerRef,
clearPosition,
});
const videoJsOptions = {
preload: 'auto',
playbackRates: videoPlaybackRates,
responsive: true,
controls: true,
html5: {
vhs: {
overrideNative: !videojs.browser.IS_ANY_SAFARI,
},
},
autoplay: autoplay,
muted: startMuted,
2020-04-16 01:21:17 +02:00
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
// fixes problem of errant CC button showing up on iOS
2021-06-25 18:26:00 +02:00
// the true fix here is to fix the m3u8 file, see: https://github.com/lbryio/lbry-desktop/pull/6315
controlBar: { subsCapsButton: false },
techOrder: ['chromecast', 'html5'],
chromecast: {
requestTitleFn: (src) => title || '',
requestSubtitleFn: (src) => channelName || '',
},
bigPlayButton: embedded, // only show big play button if embedded
2020-04-16 01:21:17 +02:00
};
// Initialize video.js
function initializeVideoPlayer(el, canAutoplayVideo) {
if (!el) return;
const vjs = videojs(el, videoJsOptions, async () => {
const player = playerRef.current;
const adapter = new playerjs.VideoJSAdapter(player);
// this seems like a weird thing to have to check for here
if (!player) return;
2021-01-26 00:50:11 +01:00
2021-12-15 19:29:06 +01:00
// runAds(internalFeatureEnabled, allowPreRoll, player, embedded);
initializeEvents();
// Replace volume bar with custom LBRY volume bar
LbryVolumeBarClass.replaceExisting(player);
2021-01-26 00:50:11 +01:00
// Add reloadSourceOnError plugin
player.reloadSourceOnError({ errorInterval: 10 });
// Initialize mobile UI.
player.mobileUi();
if (!embedded) {
window.player.bigPlayButton && window.player.bigPlayButton.hide();
} else {
const bigPlayButton = document.querySelector('.vjs-big-play-button');
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'block', 'important');
}
Chromecast.initialize(player);
// Add quality selector to player
player.hlsQualitySelector({
displayCurrentQuality: true,
});
// Add recsys plugin
if (shareTelemetry) {
player.recsys({
videoId: claimId,
userId: userId,
embedded: embedded,
});
}
// set playsinline for mobile
player.children_[0].setAttribute('playsinline', '');
if (canAutoplayVideo === true) {
// show waiting spinner as video is loading
player.addClass('vjs-waiting');
// document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
} else {
// $FlowFixMe
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'block', 'important');
}
// center play button
centerPlayButton();
// I think this is a callback function
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
onPlayerReady(player, videoNode);
adapter.ready();
// sometimes video doesnt start properly, this addresses the edge case
if (autoplay) {
const videoDiv = window.player.children_[0];
if (videoDiv) {
videoDiv.click();
}
window.player.userActive(true);
}
});
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
// summary: on firefox the focus would stick to the fullscreen button which caused buggy behavior with spacebar
vjs.on('fullscreenchange', () => document.activeElement && document.activeElement.blur());
return vjs;
}
/** instantiate videoJS and dispose of it when done with code **/
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
2020-04-16 23:43:09 +02:00
useEffect(() => {
(async function() {
// test if perms to play video are available
2022-01-06 22:20:42 +01:00
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000, inline: true });
canAutoplayVideo = canAutoplayVideo.result === true;
const vjsElement = createVideoPlayerDOM(containerRef.current);
2020-04-28 21:33:30 +02:00
// Initialize Video.js
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo);
2021-01-26 00:50:11 +01:00
// Add reference to player to global scope
window.player = vjsPlayer;
// Set reference in component state
playerRef.current = vjsPlayer;
window.addEventListener('keydown', curried_function(playerRef, containerRef));
// $FlowFixMe
document.querySelector('.vjs-control-bar').style.setProperty('opacity', '1', 'important');
// change to m3u8 if applicable
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
playerServerRef.current = response.headers.get('x-powered-by');
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
// use m3u8 source
// $FlowFixMe
vjsPlayer.src({
type: 'application/x-mpegURL',
src: response.url,
});
} else {
// use original mp4 source
// $FlowFixMe
vjsPlayer.src({
type: sourceType,
src: source,
});
}
// load video once source setup
// $FlowFixMe
vjsPlayer.load();
})();
2020-04-16 23:43:09 +02:00
// Cleanup
return () => {
window.removeEventListener('keydown', curried_function);
2020-04-16 23:43:09 +02:00
const player = playerRef.current;
if (player) {
try {
window.cast.framework.CastContext.getInstance().getCurrentSession().endSession(false);
} catch {}
player.dispose();
window.player = undefined;
}
2021-01-26 00:50:11 +01:00
};
}, [isAudio, source, reload]);
2020-04-16 01:21:17 +02:00
return (
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>
<Button
label={__('Tap to unmute')}
button="link"
icon={ICONS.VOLUME_MUTED}
className="video-js--tap-to-unmute"
onClick={unmuteAndHideHint}
ref={tapToUnmuteRef}
/>
<Button
label={__('Retry')}
button="link"
icon={ICONS.REFRESH}
className="video-js--tap-to-unmute"
onClick={retryVideoAfterFailure}
ref={tapToRetryRef}
/>
</div>
);
2020-04-16 18:10:47 +02:00
});