Reuse videojs instance between video reload, return mobile UI plugin for iOS (#1512)
* add mobile plugin back on ios * further touchups and fix ios * finish mobile functionality * dont show big play button on mobile * remove logs * proof of concept * dont go full screen on rotate * add back functionality * replace dispose event with navigate away * bugfix * turn off show if you liked button and nag only on homepage * add back old functionality * ending event not working * test here * working but needs cleanup * more player touchups * bugfix * add settings button on mobile * more touchups * more cleanups * touchup loading functionality * fix hover thumbnails * touchup and eslint fix * fix repopulation bug * change recsys event name * bugfix events * change the way buttons are removed and added * finish chapters button * refactor to use videojs methods * refactor to fix autoplay next * ux touchups * seems to be behaving properly * control bar behaving how it should * fix control bar on ios * working on flow and eslint errors * bugfix and flow fixes * bring back nudge * fix playlist button bug * remove chapter markers properly * show big play button * bugfix recsys closed event * fix analytics bug * fix embeds * bugfix * possible bugfix for kp * bugfix playlist buttons * fix issue with mobile ui plugin * fix firefox autoplay issue * fix bug for play on floating player closed * bugfix volume control for ios * instantiate new player if switching between claim types * fix flow and lint errors * fix control bar not showing up when switching sources * dispose old player if recreating * bugfix save position * reset recsys data between videos * fix audio upload posters * clear claimSrcVhs on reload * bugfix errant image previews showing up * reset player value of having already switched quality * fix watch position not being used * bugfix switching between sources not perserving position * fix save position bug * fix playlist buttons * bugfix * code cleanup and add back 5 second feature
This commit is contained in:
parent
85cb741feb
commit
87c94e3c1c
17 changed files with 388 additions and 2263 deletions
|
@ -97,12 +97,12 @@ export default function FileReactions(props: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
{channelTitle && !isCollection && (
|
||||
{channelTitle && !isCollection && (
|
||||
<NudgeFloating
|
||||
name="nudge:support-acknowledge"
|
||||
text={__('Let %channel% know you enjoyed this!', { channel: channelTitle })}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
<div className={'ratio-wrapper'}>
|
||||
<Button
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function NagLocaleSwitch(props: Props) {
|
|||
const [switchOption, setSwitchOption] = React.useState(optionToSwitch);
|
||||
|
||||
if (localeSwitchDismissed || !optionToSwitch) return null;
|
||||
if (!onFrontPage) return null;
|
||||
|
||||
let message = __(
|
||||
// If no homepage, only suggest language switch
|
||||
|
|
|
@ -98,6 +98,9 @@ function overrideHoverTooltip(player: any, tsData: TimestampData, duration: numb
|
|||
.getChild('mouseTimeDisplay')
|
||||
.getChild('timeTooltip');
|
||||
|
||||
// sometimes old 'right' rule is persisted and messes up styling
|
||||
timeTooltip.el().style.removeProperty('right');
|
||||
|
||||
timeTooltip.update = function (seekBarRect, seekBarPoint, time) {
|
||||
const values = Object.values(tsData);
|
||||
// $FlowIssue: mixed
|
||||
|
@ -153,6 +156,19 @@ function load(player: any, timestampData: TimestampData, duration: number) {
|
|||
});
|
||||
}
|
||||
|
||||
function deleteHoverInformation(player) {
|
||||
try {
|
||||
const timeTooltip = player
|
||||
.getChild('controlBar')
|
||||
.getChild('progressControl')
|
||||
.getChild('seekBar')
|
||||
.getChild('mouseTimeDisplay')
|
||||
.getChild('timeTooltip');
|
||||
|
||||
delete timeTooltip.update;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function parseAndLoad(player: any, claim: StreamClaim) {
|
||||
console.assert(claim, 'null claim');
|
||||
|
||||
|
@ -166,6 +182,8 @@ export function parseAndLoad(player: any, claim: StreamClaim) {
|
|||
if (tsData && duration) {
|
||||
load(player, tsData, duration);
|
||||
overrideHoverTooltip(player, tsData, duration);
|
||||
} else {
|
||||
deleteHoverInformation(player);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ class AutoplayNextButton extends videojs.getComponent('Button') {
|
|||
}
|
||||
|
||||
function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplayNext: boolean) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
const controlBar = player.controlBar;
|
||||
|
||||
const autoplayButton = new AutoplayNextButton(
|
||||
player,
|
||||
|
@ -55,7 +55,9 @@ function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, a
|
|||
autoplayNext
|
||||
);
|
||||
|
||||
controlBar.addChild(autoplayButton);
|
||||
if (controlBar) {
|
||||
controlBar.addChild(autoplayButton);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
|
|
@ -47,7 +47,11 @@ function addTheaterModeButton(player: Player, toggleVideoTheaterMode: () => void
|
|||
},
|
||||
});
|
||||
|
||||
controlBar.addChild(theaterMode);
|
||||
if (controlBar) {
|
||||
const existingTheatreModeButton = controlBar.getChild('TheaterModeButton');
|
||||
if (existingTheatreModeButton) controlBar.removeChild('TheaterModeButton');
|
||||
controlBar.addChild(theaterMode);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
|
@ -58,7 +62,7 @@ export default function useTheaterMode(playerRef: any, videoTheaterMode: boolean
|
|||
React.useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (player) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
const controlBar = player.controlBar;
|
||||
const theaterButton = controlBar.getChild('TheaterModeButton');
|
||||
if (theaterButton) {
|
||||
theaterButton.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||
|
|
|
@ -11,7 +11,7 @@ class PlayNextButton extends videojs.getComponent('Button') {
|
|||
}
|
||||
|
||||
export function addPlayNextButton(player: Player, playNextURI: () => void) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
const controlBar = player.controlBar;
|
||||
|
||||
const playNext = new PlayNextButton(player, {
|
||||
name: 'PlayNextButton',
|
||||
|
@ -21,5 +21,7 @@ export function addPlayNextButton(player: Player, playNextURI: () => void) {
|
|||
},
|
||||
});
|
||||
|
||||
controlBar.addChild(playNext, {}, 1);
|
||||
if (controlBar) {
|
||||
controlBar.addChild(playNext, {}, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class PlayPreviousButton extends videojs.getComponent('Button') {
|
|||
}
|
||||
|
||||
export function addPlayPreviousButton(player: Player, playPreviousURI: () => void) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
const controlBar = player.controlBar;
|
||||
|
||||
const playPrevious = new PlayPreviousButton(player, {
|
||||
name: 'PlayPreviousButton',
|
||||
|
@ -21,5 +21,7 @@ export function addPlayPreviousButton(player: Player, playPreviousURI: () => voi
|
|||
},
|
||||
});
|
||||
|
||||
controlBar.addChild(playPrevious, {}, 0);
|
||||
if (controlBar) {
|
||||
controlBar.addChild(playPrevious, {}, 0);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -277,9 +277,24 @@ class HlsQualitySelectorPlugin {
|
|||
|
||||
swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) {
|
||||
const currentTime = this.player.currentTime();
|
||||
const isAlreadyPlaying = !this.player.paused();
|
||||
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal);
|
||||
|
||||
// run this when new source is loaded
|
||||
this.player.one('loadstart', () => {
|
||||
// fixes a bug where when reusing vjs instance the player doesn't play
|
||||
// when it should and the control bar is hidden when changing quality
|
||||
this.player.currentTime(currentTime);
|
||||
if (isAlreadyPlaying) {
|
||||
this.player.play();
|
||||
} else {
|
||||
// show control bar
|
||||
this.player.addClass('vjs-has-started');
|
||||
this.player.addClass('vjs-playing');
|
||||
this.player.addClass('vjs-paused');
|
||||
}
|
||||
});
|
||||
this.player.load();
|
||||
this.player.currentTime(currentTime);
|
||||
|
||||
console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input');
|
||||
}
|
||||
|
|
|
@ -106,11 +106,13 @@ const onPlayerReady = (player, options) => {
|
|||
}
|
||||
};
|
||||
|
||||
if (videojs.browser.IS_IOS) {
|
||||
window.addEventListener('orientationchange', rotationHandler);
|
||||
} else {
|
||||
// addEventListener('orientationchange') is not a user interaction on Android
|
||||
screen.orientation.onchange = rotationHandler;
|
||||
if (player.options.enterOnRotate) {
|
||||
if (videojs.browser.IS_IOS) {
|
||||
window.addEventListener('orientationchange', rotationHandler);
|
||||
} else {
|
||||
// addEventListener('orientationchange') is not a user interaction on Android
|
||||
screen.orientation.onchange = rotationHandler;
|
||||
}
|
||||
}
|
||||
|
||||
player.on('ended', (_) => {
|
||||
|
@ -149,8 +151,7 @@ const onPlayerReady = (player, options) => {
|
|||
* Never shows if the endscreen plugin is present
|
||||
*/
|
||||
function mobileUi(options) {
|
||||
// if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
|
||||
if (videojs.browser.IS_ANDROID) {
|
||||
if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
|
||||
this.ready(() => onPlayerReady(this, videojs.mergeOptions(defaults, options)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class RecsysPlugin extends Component {
|
|||
player.on('seeked', (event) => this.onSeeked(event));
|
||||
|
||||
// Event trigger to send recsys event
|
||||
player.on('dispose', (event) => this.onDispose(event));
|
||||
player.on('playerClosed', (event) => this.onDispose(event));
|
||||
}
|
||||
|
||||
onPlay(event) {
|
||||
|
|
|
@ -113,8 +113,8 @@ const VideoJsEvents = ({
|
|||
const player = playerRef.current;
|
||||
updateMediaSession();
|
||||
|
||||
const bigPlayButton = document.querySelector('.vjs-big-play-button');
|
||||
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'none');
|
||||
// $FlowIssue
|
||||
player.bigPlayButton?.hide();
|
||||
|
||||
if (player && (player.muted() || player.volume() === 0)) {
|
||||
// The css starts as "hidden". We make it visible here without
|
||||
|
@ -222,6 +222,13 @@ const VideoJsEvents = ({
|
|||
}
|
||||
}
|
||||
|
||||
function removeControlBar() {
|
||||
setTimeout(function () {
|
||||
window.player.controlBar.el().classList.remove('vjs-transitioning-video');
|
||||
window.player.controlBar.show();
|
||||
}, 1000 * 2); // wait 2 seconds to hide control bar
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (replay && player) {
|
||||
|
@ -240,17 +247,23 @@ const VideoJsEvents = ({
|
|||
// used for tracking buffering for watchman
|
||||
player.on('tracking:buffered', doTrackingBuffered);
|
||||
|
||||
// hide forcing control bar show
|
||||
player.on('canplaythrough', function () {
|
||||
setTimeout(function () {
|
||||
// $FlowFixMe
|
||||
const vjsControlBar = document.querySelector('.vjs-control-bar');
|
||||
if (vjsControlBar) vjsControlBar.style.removeProperty('opacity');
|
||||
}, 1000 * 3); // wait 3 seconds to hit control bar
|
||||
player.on('loadstart', function () {
|
||||
if (embedded) {
|
||||
// $FlowIssue
|
||||
player.bigPlayButton?.show();
|
||||
}
|
||||
});
|
||||
player.on('playing', function () {
|
||||
// $FlowFixMe
|
||||
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
|
||||
|
||||
player.on('playing', removeControlBar);
|
||||
player.on('playerClosed', () => {
|
||||
player.off('play', onInitialPlay);
|
||||
player.off('volumechange', onVolumeChange);
|
||||
player.off('error', onError);
|
||||
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
|
||||
player.off('tracking:firstplay', doTrackingFirstPlay);
|
||||
// used for tracking buffering for watchman
|
||||
player.off('tracking:buffered', doTrackingBuffered);
|
||||
player.off('playing', removeControlBar);
|
||||
});
|
||||
// player.on('ended', onEnded);
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const VideoJsFunctions = ({ isAudio }: { isAudio: boolean }) => {
|
|||
// This seems like a poor way to generate the DOM for video.js
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.setAttribute('data-vjs-player', 'true');
|
||||
const el = document.createElement(isAudio ? 'audio' : 'video');
|
||||
const el = document.createElement('video');
|
||||
el.className = 'video-js vjs-big-play-centered ';
|
||||
wrapper.appendChild(el);
|
||||
|
||||
|
|
|
@ -29,8 +29,6 @@ import { useIsMobile } from 'effects/use-screensize';
|
|||
import { platform } from 'util/platform';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
const canAutoplay = require('./plugins/canAutoplay');
|
||||
|
||||
require('@silvermine/videojs-chromecast')(videojs);
|
||||
require('@silvermine/videojs-airplay')(videojs);
|
||||
|
||||
|
@ -46,7 +44,11 @@ export type Player = {
|
|||
hlsQualitySelector: ?any,
|
||||
i18n: (any) => void,
|
||||
// -- base videojs --
|
||||
controlBar: { addChild: (string, any) => void },
|
||||
controlBar: {
|
||||
addChild: (string | any, ?any, ?number) => void,
|
||||
getChild: (string) => void,
|
||||
removeChild: (string) => void,
|
||||
},
|
||||
loadingSpinner: any,
|
||||
autoplay: (any) => boolean,
|
||||
tech: (?boolean) => { vhs: ?any },
|
||||
|
@ -60,6 +62,7 @@ export type Player = {
|
|||
isFullscreen: () => boolean,
|
||||
muted: (?boolean) => boolean,
|
||||
on: (string, (any) => void) => void,
|
||||
off: (string, (any) => void) => void,
|
||||
one: (string, (any) => void) => void,
|
||||
play: () => Promise<any>,
|
||||
playbackRate: (?number) => number,
|
||||
|
@ -103,7 +106,6 @@ type Props = {
|
|||
activeLivestreamForChannel: any,
|
||||
doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
|
||||
};
|
||||
const VIDEOJS_CONTROL_BAR_CLASS = 'ControlBar';
|
||||
const VIDEOJS_VOLUME_PANEL_CLASS = 'VolumePanel';
|
||||
|
||||
const IS_IOS = platform.isIOS();
|
||||
|
@ -242,7 +244,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
liveTolerance: 10,
|
||||
},
|
||||
inactivityTimeout: 2000,
|
||||
autoplay: autoplay,
|
||||
muted: startMuted,
|
||||
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
|
||||
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
|
||||
|
@ -263,11 +264,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
suppressNotSupportedError: true,
|
||||
};
|
||||
|
||||
// TODO: would be nice to pull this out into functions file
|
||||
// Initialize video.js
|
||||
function initializeVideoPlayer(el, canAutoplayVideo) {
|
||||
if (!el) return;
|
||||
function initializeVideoPlayer(domElement) {
|
||||
if (!domElement) return;
|
||||
|
||||
const vjs = videojs(el, videoJsOptions, async () => {
|
||||
const vjs = videojs(domElement, videoJsOptions, async () => {
|
||||
const player = playerRef.current;
|
||||
const adapter = new playerjs.VideoJSAdapter(player);
|
||||
|
||||
|
@ -276,8 +278,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
|
||||
// runAds(internalFeatureEnabled, allowPreRoll, player, embedded);
|
||||
|
||||
initializeEvents();
|
||||
|
||||
// Replace volume bar with custom LBRY volume bar
|
||||
LbryVolumeBarClass.replaceExisting(player);
|
||||
|
||||
|
@ -285,17 +285,17 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.reloadSourceOnError({ errorInterval: 10 });
|
||||
|
||||
// Initialize mobile UI.
|
||||
player.mobileUi();
|
||||
player.mobileUi({
|
||||
fullscreen: {
|
||||
enterOnRotate: false,
|
||||
},
|
||||
touchControls: {
|
||||
seekSeconds: 10,
|
||||
},
|
||||
});
|
||||
|
||||
player.i18n();
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Add quality selector to player
|
||||
if (showQualitySelector) {
|
||||
player.hlsQualitySelector({
|
||||
|
@ -320,30 +320,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
// 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');
|
||||
}
|
||||
// immediately show control bar while video is loading
|
||||
player.userActive(true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Chromecast.initialize(player);
|
||||
player.airPlay();
|
||||
});
|
||||
|
@ -366,26 +347,72 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000, inline: true });
|
||||
canAutoplayVideo = canAutoplayVideo.result === true;
|
||||
let vjsPlayer;
|
||||
const vjsParent = document.querySelector('.video-js-parent');
|
||||
|
||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo);
|
||||
if (!vjsPlayer) {
|
||||
return;
|
||||
let canUseOldPlayer = window.oldSavedDiv && vjsParent;
|
||||
const isLivestream = isLivestreamClaim && userClaimId;
|
||||
// make an additional check and reinstantiate if switching between player types
|
||||
// switching between types on iOS causes issues and this is a faster solution
|
||||
if (vjsParent && window.player) {
|
||||
const oldVideoType = window.player.isLivestream ? 'livestream' : 'video';
|
||||
const switchFromLivestreamToVideo = oldVideoType === 'livestream' && !isLivestream;
|
||||
const switchFromVideoToLivestream = oldVideoType === 'video' && isLivestream;
|
||||
if (switchFromLivestreamToVideo || switchFromVideoToLivestream) {
|
||||
canUseOldPlayer = false;
|
||||
window.player.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Add reference to player to global scope
|
||||
window.player = vjsPlayer;
|
||||
// initialize videojs if it hasn't been done yet
|
||||
if (!canUseOldPlayer) {
|
||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||
vjsPlayer = initializeVideoPlayer(vjsElement);
|
||||
if (!vjsPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add reference to player to global scope
|
||||
window.player = vjsPlayer;
|
||||
} else {
|
||||
vjsPlayer = window.player;
|
||||
}
|
||||
|
||||
// Add recsys plugin
|
||||
if (shareTelemetry) {
|
||||
vjsPlayer.recsys.options_ = {
|
||||
videoId: claimId,
|
||||
userId: userId,
|
||||
embedded: embedded,
|
||||
};
|
||||
|
||||
vjsPlayer.recsys.lastTimeUpdate = null;
|
||||
vjsPlayer.recsys.currentTimeUpdate = null;
|
||||
vjsPlayer.recsys.inPause = false;
|
||||
vjsPlayer.recsys.watchedDuration = { total: 0, lastTimestamp: -1 };
|
||||
}
|
||||
|
||||
if (!embedded) {
|
||||
vjsPlayer.bigPlayButton && window.player.bigPlayButton.hide();
|
||||
} else {
|
||||
// $FlowIssue
|
||||
vjsPlayer.bigPlayButton?.show();
|
||||
}
|
||||
|
||||
// I think this is a callback function
|
||||
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||
|
||||
// add theatre and autoplay next button and initiate player events
|
||||
onPlayerReady(vjsPlayer, videoNode);
|
||||
|
||||
// Set reference in component state
|
||||
playerRef.current = vjsPlayer;
|
||||
|
||||
initializeEvents();
|
||||
|
||||
// volume control div, used for changing volume when scrolled over
|
||||
volumePanelRef.current = playerRef.current
|
||||
.getChild(VIDEOJS_CONTROL_BAR_CLASS)
|
||||
.getChild(VIDEOJS_VOLUME_PANEL_CLASS)
|
||||
.el();
|
||||
// $FlowIssue
|
||||
volumePanelRef.current = playerRef.current?.controlBar?.getChild(VIDEOJS_VOLUME_PANEL_CLASS)?.el();
|
||||
|
||||
const keyDownHandler = createKeyDownShortcutsHandler(playerRef, containerRef);
|
||||
const videoScrollHandler = createVideoScrollShortcutsHandler(playerRef, containerRef);
|
||||
|
@ -399,12 +426,15 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
videoScrollHandlerRef.current = videoScrollHandler;
|
||||
volumePanelScrollHandlerRef.current = volumePanelHandler;
|
||||
|
||||
const controlBar = document.querySelector('.vjs-control-bar');
|
||||
if (controlBar) {
|
||||
controlBar.style.setProperty('opacity', '1', 'important');
|
||||
}
|
||||
// $FlowIssue
|
||||
vjsPlayer.controlBar?.show();
|
||||
|
||||
if (isLivestreamClaim && userClaimId) {
|
||||
vjsPlayer.poster(poster);
|
||||
|
||||
let contentUrl;
|
||||
// TODO: pull this function into videojs-functions
|
||||
// determine which source to use and load it
|
||||
if (isLivestream) {
|
||||
vjsPlayer.isLivestream = true;
|
||||
vjsPlayer.addClass('livestreamPlayer');
|
||||
vjsPlayer.src({ type: 'application/x-mpegURL', src: livestreamVideoUrl });
|
||||
|
@ -421,23 +451,81 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
vjsPlayer.claimSrcVhs = { type: 'application/x-mpegURL', src: response.url };
|
||||
vjsPlayer.src(vjsPlayer.claimSrcVhs);
|
||||
|
||||
const trimmedPath = response.url.substring(0, response.url.lastIndexOf('/'));
|
||||
const thumbnailPath = trimmedPath + '/stream_sprite.vtt';
|
||||
|
||||
// disable thumbnails on mobile for now
|
||||
if (!IS_MOBILE) {
|
||||
vjsPlayer.vttThumbnails({
|
||||
src: thumbnailPath,
|
||||
showTimestamp: true,
|
||||
});
|
||||
}
|
||||
contentUrl = response.url;
|
||||
} else {
|
||||
vjsPlayer.src(vjsPlayer.claimSrcOriginal);
|
||||
}
|
||||
}
|
||||
|
||||
// bugfix thumbnails showing up if new video doesn't have them
|
||||
if (typeof vjsPlayer.vttThumbnails.detach === 'function') {
|
||||
vjsPlayer.vttThumbnails.detach();
|
||||
}
|
||||
|
||||
// initialize hover thumbnails
|
||||
if (contentUrl) {
|
||||
const trimmedPath = contentUrl.substring(0, contentUrl.lastIndexOf('/'));
|
||||
const thumbnailPath = trimmedPath + '/stream_sprite.vtt';
|
||||
|
||||
// progress bar hover thumbnails
|
||||
if (!IS_MOBILE) {
|
||||
// if src is a function, it's already been initialized
|
||||
if (typeof vjsPlayer.vttThumbnails.src === 'function') {
|
||||
vjsPlayer.vttThumbnails.src(thumbnailPath);
|
||||
} else {
|
||||
// otherwise, initialize plugin
|
||||
vjsPlayer.vttThumbnails({
|
||||
src: thumbnailPath,
|
||||
showTimestamp: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vjsPlayer.load();
|
||||
|
||||
if (canUseOldPlayer) {
|
||||
// $FlowIssue
|
||||
document.querySelector('.video-js-parent')?.append(window.oldSavedDiv);
|
||||
}
|
||||
|
||||
// allow tap to unmute if no perms on iOS
|
||||
if (autoplay && !embedded) {
|
||||
const promise = vjsPlayer.play();
|
||||
|
||||
window.player.userActive(true);
|
||||
|
||||
if (promise !== undefined) {
|
||||
promise
|
||||
.then((_) => {
|
||||
// $FlowIssue
|
||||
vjsPlayer?.controlBar.el().classList.add('vjs-transitioning-video');
|
||||
})
|
||||
.catch((error) => {
|
||||
const noPermissionError = typeof error === 'object' && error.name && error.name === 'NotAllowedError';
|
||||
|
||||
if (noPermissionError) {
|
||||
if (IS_IOS) {
|
||||
// autoplay not allowed, mute video, play and show 'tap to unmute' button
|
||||
// $FlowIssue
|
||||
vjsPlayer?.muted(true);
|
||||
// $FlowIssue
|
||||
vjsPlayer?.play();
|
||||
// $FlowIssue
|
||||
document.querySelector('.video-js--tap-to-unmute')?.style.setProperty('visibility', 'visible');
|
||||
// $FlowIssue
|
||||
document
|
||||
.querySelector('.video-js--tap-to-unmute')
|
||||
?.style.setProperty('display', 'inline', 'important');
|
||||
} else {
|
||||
// $FlowIssue
|
||||
vjsPlayer?.bigPlayButton?.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// fix invisible vidcrunch overlay on IOS << TODO: does not belong here. Move to ads.jsx (#739)
|
||||
if (IS_IOS) {
|
||||
// ads video player
|
||||
|
@ -477,14 +565,45 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
volumePanelRef.current.removeEventListener('wheel', volumePanelScrollHandlerRef.current);
|
||||
}
|
||||
|
||||
const chapterMarkers = document.getElementsByClassName('vjs-chapter-marker');
|
||||
while (chapterMarkers.length > 0) {
|
||||
// $FlowIssue
|
||||
chapterMarkers[0].parentNode?.removeChild(chapterMarkers[0]);
|
||||
}
|
||||
|
||||
const player = playerRef.current;
|
||||
if (player) {
|
||||
try {
|
||||
window.cast.framework.CastContext.getInstance().getCurrentSession().endSession(false);
|
||||
} catch {}
|
||||
|
||||
player.dispose();
|
||||
window.player = undefined;
|
||||
window.player.switchedFromDefaultQuality = false;
|
||||
|
||||
window.player.userActive(false);
|
||||
window.player.pause();
|
||||
|
||||
if (IS_IOS) {
|
||||
// $FlowIssue
|
||||
window.player.controlBar?.playToggle?.hide();
|
||||
}
|
||||
|
||||
// $FlowIssue
|
||||
window.player?.controlBar?.getChild('ChaptersButton')?.hide();
|
||||
|
||||
// this solves an issue with portrait videos
|
||||
// $FlowIssue
|
||||
const videoDiv = window.player?.tech_?.el(); // video element
|
||||
if (videoDiv) videoDiv.style.top = '0px';
|
||||
|
||||
window.player.controlBar.el().classList.add('vjs-transitioning-video');
|
||||
|
||||
window.oldSavedDiv = window.player.el();
|
||||
|
||||
window.player.trigger('playerClosed');
|
||||
|
||||
window.player.currentTime(0);
|
||||
|
||||
window.player.claimSrcVhs = null;
|
||||
}
|
||||
};
|
||||
}, [isAudio, source, reload, userClaimId, isLivestreamClaim]);
|
||||
|
|
|
@ -29,12 +29,15 @@ import debounce from 'util/debounce';
|
|||
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
||||
import useInterval from 'effects/use-interval';
|
||||
import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors';
|
||||
import { platform } from 'util/platform';
|
||||
import RecSys from 'recsys';
|
||||
|
||||
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||
// const PLAY_TIMEOUT_LIMIT = 2000;
|
||||
const PLAY_POSITION_SAVE_INTERVAL_MS = 15000;
|
||||
|
||||
const IS_IOS = platform.isIOS();
|
||||
|
||||
type Props = {
|
||||
position: number,
|
||||
changeVolume: (number) => void,
|
||||
|
@ -161,6 +164,7 @@ function VideoViewer(props: Props) {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (isPlaying) {
|
||||
// save the updated watch time
|
||||
doSetContentHistoryItem(claim.permanent_url);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
@ -209,6 +213,7 @@ function VideoViewer(props: Props) {
|
|||
|
||||
const doPlay = useCallback(
|
||||
(playUri) => {
|
||||
if (!playUri) return;
|
||||
setDoNavigate(false);
|
||||
if (!isFloating) {
|
||||
const navigateUrl = formatLbryUrlForWeb(playUri);
|
||||
|
@ -227,27 +232,23 @@ function VideoViewer(props: Props) {
|
|||
useEffect(() => {
|
||||
if (!doNavigate) return;
|
||||
|
||||
if (playNextUrl) {
|
||||
if (permanentUrl !== nextRecommendedUri) {
|
||||
if (nextRecommendedUri) {
|
||||
doPlay(nextRecommendedUri);
|
||||
}
|
||||
} else {
|
||||
setReplay(true);
|
||||
}
|
||||
} else {
|
||||
if (videoNode) {
|
||||
const currentTime = videoNode.currentTime;
|
||||
const shouldPlayNextUrl = playNextUrl && nextRecommendedUri && permanentUrl !== nextRecommendedUri;
|
||||
const shouldPlayPreviousUrl = !playNextUrl && previousListUri && permanentUrl !== previousListUri;
|
||||
|
||||
if (currentTime <= 5) {
|
||||
if (previousListUri && permanentUrl !== previousListUri) doPlay(previousListUri);
|
||||
} else {
|
||||
videoNode.currentTime = 0;
|
||||
}
|
||||
setDoNavigate(false);
|
||||
}
|
||||
// play next video if someone hits Next button
|
||||
if (shouldPlayNextUrl) {
|
||||
doPlay(nextRecommendedUri);
|
||||
// rewind if video is over 5 seconds and they hit the back button
|
||||
} else if (videoNode && videoNode.currentTime > 5) {
|
||||
videoNode.currentTime = 0;
|
||||
// move to previous video when they hit back button if behind 5 seconds
|
||||
} else if (shouldPlayPreviousUrl) {
|
||||
doPlay(previousListUri);
|
||||
} else {
|
||||
setReplay(true);
|
||||
}
|
||||
if (!ended) setDoNavigate(false);
|
||||
|
||||
setDoNavigate(false);
|
||||
setEnded(false);
|
||||
setPlayNextUrl(true);
|
||||
}, [
|
||||
|
@ -262,6 +263,7 @@ function VideoViewer(props: Props) {
|
|||
videoNode,
|
||||
]);
|
||||
|
||||
// functionality to run on video end
|
||||
React.useEffect(() => {
|
||||
if (!ended) return;
|
||||
|
||||
|
@ -274,13 +276,21 @@ function VideoViewer(props: Props) {
|
|||
|
||||
if (embedded) {
|
||||
setIsEndedEmbed(true);
|
||||
// show autoplay countdown div if not playlist
|
||||
} else if (!collectionId && autoplayNext) {
|
||||
setShowAutoplayCountdown(true);
|
||||
// if a playlist, navigate to next item
|
||||
} else if (collectionId) {
|
||||
setDoNavigate(true);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
|
||||
if (IS_IOS && !autoplayNext) {
|
||||
// show play button on ios if video is paused with no autoplay on
|
||||
// $FlowFixMe
|
||||
document.querySelector('.vjs-touch-overlay')?.classList.add('show-play-toggle'); // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
}, [adUrl, autoplayNext, clearPosition, collectionId, embedded, ended, setAdUrl, uri]);
|
||||
|
||||
// MORE ON PLAY STUFF
|
||||
|
@ -300,7 +310,7 @@ function VideoViewer(props: Props) {
|
|||
analytics.videoIsPlaying(false, player);
|
||||
}
|
||||
|
||||
function onDispose(event, player) {
|
||||
function onPlayerClosed(event, player) {
|
||||
handlePosition(player);
|
||||
analytics.videoIsPlaying(false, player);
|
||||
}
|
||||
|
@ -328,6 +338,7 @@ function VideoViewer(props: Props) {
|
|||
};
|
||||
|
||||
const onPlayerReady = useCallback((player: Player, videoNode: any) => {
|
||||
// add buttons and initialize some settings for the player
|
||||
if (!embedded) {
|
||||
setVideoNode(videoNode);
|
||||
player.muted(muted);
|
||||
|
@ -335,6 +346,21 @@ function VideoViewer(props: Props) {
|
|||
player.playbackRate(videoPlaybackRate);
|
||||
if (!isMarkdownOrComment) {
|
||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||
// if part of a playlist
|
||||
|
||||
// remove old play next/previous buttons if they exist
|
||||
const controlBar = player.controlBar;
|
||||
if (controlBar) {
|
||||
const existingPlayNextButton = controlBar.getChild('PlayNextButton');
|
||||
if (existingPlayNextButton) controlBar.removeChild('PlayNextButton');
|
||||
|
||||
const existingPlayPreviousButton = controlBar.getChild('PlayPreviousButton');
|
||||
if (existingPlayPreviousButton) controlBar.removeChild('PlayPreviousButton');
|
||||
|
||||
const existingAutoplayButton = controlBar.getChild('AutoplayNextButton');
|
||||
if (existingAutoplayButton) controlBar.removeChild('AutoplayNextButton');
|
||||
}
|
||||
|
||||
if (collectionId) {
|
||||
addPlayNextButton(player, doPlayNext);
|
||||
addPlayPreviousButton(player, doPlayPrevious);
|
||||
|
@ -349,74 +375,36 @@ function VideoViewer(props: Props) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// currently not being used, but leaving for time being
|
||||
// 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)
|
||||
// );
|
||||
//
|
||||
// // if user hasn't interacted with document, mute video and play it
|
||||
// Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
||||
// console.log(error);
|
||||
// console.log(playPromise);
|
||||
//
|
||||
// const noPermissionError = typeof error === 'object' && error.name && error.name === 'NotAllowedError';
|
||||
// const isATimeoutError = error === PLAY_TIMEOUT_ERROR;
|
||||
//
|
||||
// if (noPermissionError) {
|
||||
// // if (player.paused()) {
|
||||
// // document.querySelector('.vjs-big-play-button').style.setProperty('display', 'block', 'important');
|
||||
// // }
|
||||
//
|
||||
// centerPlayButton();
|
||||
//
|
||||
// // to turn muted autoplay on
|
||||
// // if (player.autoplay() && !player.muted()) {
|
||||
// // player.muted(true);
|
||||
// // player.play();
|
||||
// // }
|
||||
// }
|
||||
// setIsPlaying(false);
|
||||
// });
|
||||
// }
|
||||
|
||||
// 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));
|
||||
const restorePlaybackRateEvent = () => restorePlaybackRate(player);
|
||||
|
||||
// Override the "auto" algorithm to post-process the result
|
||||
player.on('loadedmetadata', () => {
|
||||
const overrideAutoAlgorithm = () => {
|
||||
const vhs = player.tech(true).vhs;
|
||||
if (vhs) {
|
||||
// https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884
|
||||
vhs.selectPlaylist = lastBandwidthSelector;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
player.on('ended', () => setEnded(true));
|
||||
player.on('play', onPlay);
|
||||
player.on('pause', (event) => onPause(event, player));
|
||||
player.on('dispose', (event) => onDispose(event, player));
|
||||
|
||||
player.on('error', () => {
|
||||
const onPauseEvent = (event) => onPause(event, player);
|
||||
const onPlayerClosedEvent = (event) => onPlayerClosed(event, player);
|
||||
const onVolumeChange = () => {
|
||||
if (player) {
|
||||
updateVolumeState(player.volume(), player.muted());
|
||||
}
|
||||
};
|
||||
const onPlayerEnded = () => setEnded(true);
|
||||
const onError = () => {
|
||||
const error = player.error();
|
||||
if (error) {
|
||||
analytics.sentryError('Video.js error', error);
|
||||
}
|
||||
});
|
||||
player.on('volumechange', () => {
|
||||
if (player) {
|
||||
updateVolumeState(player.volume(), player.muted());
|
||||
}
|
||||
});
|
||||
player.on('ratechange', () => {
|
||||
};
|
||||
const onRateChange = () => {
|
||||
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.
|
||||
|
@ -425,11 +413,43 @@ function VideoViewer(props: Props) {
|
|||
// [ ] Ideally, the controlBar should be hidden to prevent users from changing the rate while loading.
|
||||
setVideoPlaybackRate(player.playbackRate());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (position && !isLivestreamClaim) {
|
||||
player.currentTime(position);
|
||||
}
|
||||
const moveToPosition = () => {
|
||||
// update current time based on previous position
|
||||
if (position && !isLivestreamClaim) {
|
||||
player.currentTime(position);
|
||||
}
|
||||
};
|
||||
|
||||
// load events onto player
|
||||
player.on('play', onPlay);
|
||||
player.on('pause', onPauseEvent);
|
||||
player.on('playerClosed', onPlayerClosedEvent);
|
||||
player.on('ended', onPlayerEnded);
|
||||
player.on('error', onError);
|
||||
player.on('volumechange', onVolumeChange);
|
||||
player.on('ratechange', onRateChange);
|
||||
player.on('loadedmetadata', overrideAutoAlgorithm);
|
||||
player.on('loadedmetadata', restorePlaybackRateEvent);
|
||||
player.one('loadedmetadata', moveToPosition);
|
||||
|
||||
const cancelOldEvents = () => {
|
||||
player.off('play', onPlay);
|
||||
player.off('pause', onPauseEvent);
|
||||
player.off('playerClosed', onPlayerClosedEvent);
|
||||
player.off('ended', onPlayerEnded);
|
||||
player.off('error', onError);
|
||||
player.off('volumechange', onVolumeChange);
|
||||
player.off('ratechange', onRateChange);
|
||||
player.off('loadedmetadata', overrideAutoAlgorithm);
|
||||
player.off('loadedmetadata', restorePlaybackRateEvent);
|
||||
player.off('playerClosed', cancelOldEvents);
|
||||
player.off('loadedmetadata', moveToPosition);
|
||||
};
|
||||
|
||||
// turn off old events to prevent duplicate runs
|
||||
player.on('playerClosed', cancelOldEvents);
|
||||
|
||||
Chapters.parseAndLoad(player, claim);
|
||||
|
||||
|
|
|
@ -331,7 +331,7 @@ $control-bar-popup-font-size: 0.8rem;
|
|||
|
||||
// TODO: make sure there's no bad side effects of this
|
||||
button.vjs-big-play-button {
|
||||
display: none !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-big-play-centered {
|
||||
|
@ -465,3 +465,16 @@ button.vjs-big-play-button {
|
|||
background-color: var(--color-error);
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
// don't show Tap To Unmute button on mobile miniplayer
|
||||
.content__viewer--floating.content__viewer--mobile {
|
||||
.video-js--tap-to-unmute {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-transitioning-video {
|
||||
opacity: 1 !important;
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
|
|
@ -7374,6 +7374,7 @@ flatten@^1.0.2:
|
|||
flow-bin@^0.97.0:
|
||||
version "0.97.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.97.0.tgz#036ffcfc27503367a9d906ec9d843a0aa6f6bb83"
|
||||
integrity sha512-jXjD05gkatLuC4+e28frH1hZoRwr1iASP6oJr61Q64+kR4kmzaS+AdFBhYgoYS5kpoe4UzwDebWK8ETQFNh00w==
|
||||
|
||||
flow-typed@^2.3.0:
|
||||
version "2.6.2"
|
||||
|
|
Loading…
Reference in a new issue