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:
mayeaux 2022-06-10 18:18:58 +02:00 committed by GitHub
parent 85cb741feb
commit 87c94e3c1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 388 additions and 2263 deletions

View file

@ -97,12 +97,12 @@ export default function FileReactions(props: Props) {
return ( return (
<> <>
{channelTitle && !isCollection && ( {channelTitle && !isCollection && (
<NudgeFloating <NudgeFloating
name="nudge:support-acknowledge" name="nudge:support-acknowledge"
text={__('Let %channel% know you enjoyed this!', { channel: channelTitle })} text={__('Let %channel% know you enjoyed this!', { channel: channelTitle })}
/> />
)} )}
<div className={'ratio-wrapper'}> <div className={'ratio-wrapper'}>
<Button <Button

View file

@ -57,6 +57,7 @@ export default function NagLocaleSwitch(props: Props) {
const [switchOption, setSwitchOption] = React.useState(optionToSwitch); const [switchOption, setSwitchOption] = React.useState(optionToSwitch);
if (localeSwitchDismissed || !optionToSwitch) return null; if (localeSwitchDismissed || !optionToSwitch) return null;
if (!onFrontPage) return null;
let message = __( let message = __(
// If no homepage, only suggest language switch // If no homepage, only suggest language switch

View file

@ -98,6 +98,9 @@ function overrideHoverTooltip(player: any, tsData: TimestampData, duration: numb
.getChild('mouseTimeDisplay') .getChild('mouseTimeDisplay')
.getChild('timeTooltip'); .getChild('timeTooltip');
// sometimes old 'right' rule is persisted and messes up styling
timeTooltip.el().style.removeProperty('right');
timeTooltip.update = function (seekBarRect, seekBarPoint, time) { timeTooltip.update = function (seekBarRect, seekBarPoint, time) {
const values = Object.values(tsData); const values = Object.values(tsData);
// $FlowIssue: mixed // $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) { export function parseAndLoad(player: any, claim: StreamClaim) {
console.assert(claim, 'null claim'); console.assert(claim, 'null claim');
@ -166,6 +182,8 @@ export function parseAndLoad(player: any, claim: StreamClaim) {
if (tsData && duration) { if (tsData && duration) {
load(player, tsData, duration); load(player, tsData, duration);
overrideHoverTooltip(player, tsData, duration); overrideHoverTooltip(player, tsData, duration);
} else {
deleteHoverInformation(player);
} }
} }

View file

@ -41,7 +41,7 @@ class AutoplayNextButton extends videojs.getComponent('Button') {
} }
function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplayNext: boolean) { function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplayNext: boolean) {
const controlBar = player.getChild('controlBar'); const controlBar = player.controlBar;
const autoplayButton = new AutoplayNextButton( const autoplayButton = new AutoplayNextButton(
player, player,
@ -55,7 +55,9 @@ function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, a
autoplayNext autoplayNext
); );
controlBar.addChild(autoplayButton); if (controlBar) {
controlBar.addChild(autoplayButton);
}
} }
// **************************************************************************** // ****************************************************************************

View file

@ -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(() => { React.useEffect(() => {
const player = playerRef.current; const player = playerRef.current;
if (player) { if (player) {
const controlBar = player.getChild('controlBar'); const controlBar = player.controlBar;
const theaterButton = controlBar.getChild('TheaterModeButton'); const theaterButton = controlBar.getChild('TheaterModeButton');
if (theaterButton) { if (theaterButton) {
theaterButton.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)')); theaterButton.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));

View file

@ -11,7 +11,7 @@ class PlayNextButton extends videojs.getComponent('Button') {
} }
export function addPlayNextButton(player: Player, playNextURI: () => void) { export function addPlayNextButton(player: Player, playNextURI: () => void) {
const controlBar = player.getChild('controlBar'); const controlBar = player.controlBar;
const playNext = new PlayNextButton(player, { const playNext = new PlayNextButton(player, {
name: 'PlayNextButton', name: 'PlayNextButton',
@ -21,5 +21,7 @@ export function addPlayNextButton(player: Player, playNextURI: () => void) {
}, },
}); });
controlBar.addChild(playNext, {}, 1); if (controlBar) {
controlBar.addChild(playNext, {}, 1);
}
} }

View file

@ -11,7 +11,7 @@ class PlayPreviousButton extends videojs.getComponent('Button') {
} }
export function addPlayPreviousButton(player: Player, playPreviousURI: () => void) { export function addPlayPreviousButton(player: Player, playPreviousURI: () => void) {
const controlBar = player.getChild('controlBar'); const controlBar = player.controlBar;
const playPrevious = new PlayPreviousButton(player, { const playPrevious = new PlayPreviousButton(player, {
name: 'PlayPreviousButton', name: 'PlayPreviousButton',
@ -21,5 +21,7 @@ export function addPlayPreviousButton(player: Player, playPreviousURI: () => voi
}, },
}); });
controlBar.addChild(playPrevious, {}, 0); if (controlBar) {
controlBar.addChild(playPrevious, {}, 0);
}
} }

View file

@ -277,9 +277,24 @@ class HlsQualitySelectorPlugin {
swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) { swapSrcTo(mode = QUALITY_OPTIONS.ORIGINAL) {
const currentTime = this.player.currentTime(); const currentTime = this.player.currentTime();
const isAlreadyPlaying = !this.player.paused();
this.player.src(mode === 'vhs' ? this.player.claimSrcVhs : this.player.claimSrcOriginal); 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.load();
this.player.currentTime(currentTime);
console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input'); console.assert(mode === 'vhs' || mode === QUALITY_OPTIONS.ORIGINAL, 'Unexpected input');
} }

View file

@ -106,11 +106,13 @@ const onPlayerReady = (player, options) => {
} }
}; };
if (videojs.browser.IS_IOS) { if (player.options.enterOnRotate) {
window.addEventListener('orientationchange', rotationHandler); if (videojs.browser.IS_IOS) {
} else { window.addEventListener('orientationchange', rotationHandler);
// addEventListener('orientationchange') is not a user interaction on Android } else {
screen.orientation.onchange = rotationHandler; // addEventListener('orientationchange') is not a user interaction on Android
screen.orientation.onchange = rotationHandler;
}
} }
player.on('ended', (_) => { player.on('ended', (_) => {
@ -149,8 +151,7 @@ const onPlayerReady = (player, options) => {
* Never shows if the endscreen plugin is present * Never shows if the endscreen plugin is present
*/ */
function mobileUi(options) { function mobileUi(options) {
// if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) { if (videojs.browser.IS_ANDROID || videojs.browser.IS_IOS) {
if (videojs.browser.IS_ANDROID) {
this.ready(() => onPlayerReady(this, videojs.mergeOptions(defaults, options))); this.ready(() => onPlayerReady(this, videojs.mergeOptions(defaults, options)));
} }
} }

View file

@ -66,7 +66,7 @@ class RecsysPlugin extends Component {
player.on('seeked', (event) => this.onSeeked(event)); player.on('seeked', (event) => this.onSeeked(event));
// Event trigger to send recsys event // Event trigger to send recsys event
player.on('dispose', (event) => this.onDispose(event)); player.on('playerClosed', (event) => this.onDispose(event));
} }
onPlay(event) { onPlay(event) {

View file

@ -113,8 +113,8 @@ const VideoJsEvents = ({
const player = playerRef.current; const player = playerRef.current;
updateMediaSession(); updateMediaSession();
const bigPlayButton = document.querySelector('.vjs-big-play-button'); // $FlowIssue
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'none'); player.bigPlayButton?.hide();
if (player && (player.muted() || player.volume() === 0)) { if (player && (player.muted() || player.volume() === 0)) {
// The css starts as "hidden". We make it visible here without // 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(() => { useEffect(() => {
const player = playerRef.current; const player = playerRef.current;
if (replay && player) { if (replay && player) {
@ -240,17 +247,23 @@ const VideoJsEvents = ({
// used for tracking buffering for watchman // used for tracking buffering for watchman
player.on('tracking:buffered', doTrackingBuffered); player.on('tracking:buffered', doTrackingBuffered);
// hide forcing control bar show player.on('loadstart', function () {
player.on('canplaythrough', function () { if (embedded) {
setTimeout(function () { // $FlowIssue
// $FlowFixMe player.bigPlayButton?.show();
const vjsControlBar = document.querySelector('.vjs-control-bar'); }
if (vjsControlBar) vjsControlBar.style.removeProperty('opacity');
}, 1000 * 3); // wait 3 seconds to hit control bar
}); });
player.on('playing', function () {
// $FlowFixMe player.on('playing', removeControlBar);
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important'); 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); // player.on('ended', onEnded);

View file

@ -8,7 +8,7 @@ const VideoJsFunctions = ({ isAudio }: { isAudio: boolean }) => {
// This seems like a poor way to generate the DOM for video.js // This seems like a poor way to generate the DOM for video.js
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.setAttribute('data-vjs-player', 'true'); 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 '; el.className = 'video-js vjs-big-play-centered ';
wrapper.appendChild(el); wrapper.appendChild(el);

View file

@ -29,8 +29,6 @@ import { useIsMobile } from 'effects/use-screensize';
import { platform } from 'util/platform'; import { platform } from 'util/platform';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
const canAutoplay = require('./plugins/canAutoplay');
require('@silvermine/videojs-chromecast')(videojs); require('@silvermine/videojs-chromecast')(videojs);
require('@silvermine/videojs-airplay')(videojs); require('@silvermine/videojs-airplay')(videojs);
@ -46,7 +44,11 @@ export type Player = {
hlsQualitySelector: ?any, hlsQualitySelector: ?any,
i18n: (any) => void, i18n: (any) => void,
// -- base videojs -- // -- base videojs --
controlBar: { addChild: (string, any) => void }, controlBar: {
addChild: (string | any, ?any, ?number) => void,
getChild: (string) => void,
removeChild: (string) => void,
},
loadingSpinner: any, loadingSpinner: any,
autoplay: (any) => boolean, autoplay: (any) => boolean,
tech: (?boolean) => { vhs: ?any }, tech: (?boolean) => { vhs: ?any },
@ -60,6 +62,7 @@ export type Player = {
isFullscreen: () => boolean, isFullscreen: () => boolean,
muted: (?boolean) => boolean, muted: (?boolean) => boolean,
on: (string, (any) => void) => void, on: (string, (any) => void) => void,
off: (string, (any) => void) => void,
one: (string, (any) => void) => void, one: (string, (any) => void) => void,
play: () => Promise<any>, play: () => Promise<any>,
playbackRate: (?number) => number, playbackRate: (?number) => number,
@ -103,7 +106,6 @@ type Props = {
activeLivestreamForChannel: any, activeLivestreamForChannel: any,
doToast: ({ message: string, linkText: string, linkTarget: string }) => void, doToast: ({ message: string, linkText: string, linkTarget: string }) => void,
}; };
const VIDEOJS_CONTROL_BAR_CLASS = 'ControlBar';
const VIDEOJS_VOLUME_PANEL_CLASS = 'VolumePanel'; const VIDEOJS_VOLUME_PANEL_CLASS = 'VolumePanel';
const IS_IOS = platform.isIOS(); const IS_IOS = platform.isIOS();
@ -242,7 +244,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
liveTolerance: 10, liveTolerance: 10,
}, },
inactivityTimeout: 2000, inactivityTimeout: 2000,
autoplay: autoplay,
muted: startMuted, muted: startMuted,
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA }, plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
@ -263,11 +264,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
suppressNotSupportedError: true, suppressNotSupportedError: true,
}; };
// TODO: would be nice to pull this out into functions file
// Initialize video.js // Initialize video.js
function initializeVideoPlayer(el, canAutoplayVideo) { function initializeVideoPlayer(domElement) {
if (!el) return; if (!domElement) return;
const vjs = videojs(el, videoJsOptions, async () => { const vjs = videojs(domElement, videoJsOptions, async () => {
const player = playerRef.current; const player = playerRef.current;
const adapter = new playerjs.VideoJSAdapter(player); const adapter = new playerjs.VideoJSAdapter(player);
@ -276,8 +278,6 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// runAds(internalFeatureEnabled, allowPreRoll, player, embedded); // runAds(internalFeatureEnabled, allowPreRoll, player, embedded);
initializeEvents();
// Replace volume bar with custom LBRY volume bar // Replace volume bar with custom LBRY volume bar
LbryVolumeBarClass.replaceExisting(player); LbryVolumeBarClass.replaceExisting(player);
@ -285,17 +285,17 @@ export default React.memo<Props>(function VideoJs(props: Props) {
player.reloadSourceOnError({ errorInterval: 10 }); player.reloadSourceOnError({ errorInterval: 10 });
// Initialize mobile UI. // Initialize mobile UI.
player.mobileUi(); player.mobileUi({
fullscreen: {
enterOnRotate: false,
},
touchControls: {
seekSeconds: 10,
},
});
player.i18n(); 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 // Add quality selector to player
if (showQualitySelector) { if (showQualitySelector) {
player.hlsQualitySelector({ player.hlsQualitySelector({
@ -320,30 +320,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// set playsinline for mobile // set playsinline for mobile
player.children_[0].setAttribute('playsinline', ''); player.children_[0].setAttribute('playsinline', '');
if (canAutoplayVideo === true) { // immediately show control bar while video is loading
// show waiting spinner as video is loading player.userActive(true);
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');
}
// I think this is a callback function
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
onPlayerReady(player, videoNode);
adapter.ready(); 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); Chromecast.initialize(player);
player.airPlay(); 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. // This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
useEffect(() => { useEffect(() => {
(async function () { (async function () {
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000, inline: true }); let vjsPlayer;
canAutoplayVideo = canAutoplayVideo.result === true; const vjsParent = document.querySelector('.video-js-parent');
const vjsElement = createVideoPlayerDOM(containerRef.current); let canUseOldPlayer = window.oldSavedDiv && vjsParent;
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo); const isLivestream = isLivestreamClaim && userClaimId;
if (!vjsPlayer) { // make an additional check and reinstantiate if switching between player types
return; // 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 // initialize videojs if it hasn't been done yet
window.player = vjsPlayer; 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 // Set reference in component state
playerRef.current = vjsPlayer; playerRef.current = vjsPlayer;
initializeEvents();
// volume control div, used for changing volume when scrolled over // volume control div, used for changing volume when scrolled over
volumePanelRef.current = playerRef.current // $FlowIssue
.getChild(VIDEOJS_CONTROL_BAR_CLASS) volumePanelRef.current = playerRef.current?.controlBar?.getChild(VIDEOJS_VOLUME_PANEL_CLASS)?.el();
.getChild(VIDEOJS_VOLUME_PANEL_CLASS)
.el();
const keyDownHandler = createKeyDownShortcutsHandler(playerRef, containerRef); const keyDownHandler = createKeyDownShortcutsHandler(playerRef, containerRef);
const videoScrollHandler = createVideoScrollShortcutsHandler(playerRef, containerRef); const videoScrollHandler = createVideoScrollShortcutsHandler(playerRef, containerRef);
@ -399,12 +426,15 @@ export default React.memo<Props>(function VideoJs(props: Props) {
videoScrollHandlerRef.current = videoScrollHandler; videoScrollHandlerRef.current = videoScrollHandler;
volumePanelScrollHandlerRef.current = volumePanelHandler; volumePanelScrollHandlerRef.current = volumePanelHandler;
const controlBar = document.querySelector('.vjs-control-bar'); // $FlowIssue
if (controlBar) { vjsPlayer.controlBar?.show();
controlBar.style.setProperty('opacity', '1', 'important');
}
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.isLivestream = true;
vjsPlayer.addClass('livestreamPlayer'); vjsPlayer.addClass('livestreamPlayer');
vjsPlayer.src({ type: 'application/x-mpegURL', src: livestreamVideoUrl }); 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.claimSrcVhs = { type: 'application/x-mpegURL', src: response.url };
vjsPlayer.src(vjsPlayer.claimSrcVhs); vjsPlayer.src(vjsPlayer.claimSrcVhs);
const trimmedPath = response.url.substring(0, response.url.lastIndexOf('/')); contentUrl = response.url;
const thumbnailPath = trimmedPath + '/stream_sprite.vtt';
// disable thumbnails on mobile for now
if (!IS_MOBILE) {
vjsPlayer.vttThumbnails({
src: thumbnailPath,
showTimestamp: true,
});
}
} else { } else {
vjsPlayer.src(vjsPlayer.claimSrcOriginal); 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(); 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) // fix invisible vidcrunch overlay on IOS << TODO: does not belong here. Move to ads.jsx (#739)
if (IS_IOS) { if (IS_IOS) {
// ads video player // ads video player
@ -477,14 +565,45 @@ export default React.memo<Props>(function VideoJs(props: Props) {
volumePanelRef.current.removeEventListener('wheel', volumePanelScrollHandlerRef.current); 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; const player = playerRef.current;
if (player) { if (player) {
try { try {
window.cast.framework.CastContext.getInstance().getCurrentSession().endSession(false); window.cast.framework.CastContext.getInstance().getCurrentSession().endSession(false);
} catch {} } catch {}
player.dispose(); window.player.switchedFromDefaultQuality = false;
window.player = undefined;
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]); }, [isAudio, source, reload, userClaimId, isLivestreamClaim]);

View file

@ -29,12 +29,15 @@ import debounce from 'util/debounce';
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import useInterval from 'effects/use-interval'; import useInterval from 'effects/use-interval';
import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors'; import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors';
import { platform } from 'util/platform';
import RecSys from 'recsys'; import RecSys from 'recsys';
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; // const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
// const PLAY_TIMEOUT_LIMIT = 2000; // const PLAY_TIMEOUT_LIMIT = 2000;
const PLAY_POSITION_SAVE_INTERVAL_MS = 15000; const PLAY_POSITION_SAVE_INTERVAL_MS = 15000;
const IS_IOS = platform.isIOS();
type Props = { type Props = {
position: number, position: number,
changeVolume: (number) => void, changeVolume: (number) => void,
@ -161,6 +164,7 @@ function VideoViewer(props: Props) {
React.useEffect(() => { React.useEffect(() => {
if (isPlaying) { if (isPlaying) {
// save the updated watch time
doSetContentHistoryItem(claim.permanent_url); doSetContentHistoryItem(claim.permanent_url);
} }
}, [isPlaying]); }, [isPlaying]);
@ -209,6 +213,7 @@ function VideoViewer(props: Props) {
const doPlay = useCallback( const doPlay = useCallback(
(playUri) => { (playUri) => {
if (!playUri) return;
setDoNavigate(false); setDoNavigate(false);
if (!isFloating) { if (!isFloating) {
const navigateUrl = formatLbryUrlForWeb(playUri); const navigateUrl = formatLbryUrlForWeb(playUri);
@ -227,27 +232,23 @@ function VideoViewer(props: Props) {
useEffect(() => { useEffect(() => {
if (!doNavigate) return; if (!doNavigate) return;
if (playNextUrl) { const shouldPlayNextUrl = playNextUrl && nextRecommendedUri && permanentUrl !== nextRecommendedUri;
if (permanentUrl !== nextRecommendedUri) { const shouldPlayPreviousUrl = !playNextUrl && previousListUri && permanentUrl !== previousListUri;
if (nextRecommendedUri) {
doPlay(nextRecommendedUri);
}
} else {
setReplay(true);
}
} else {
if (videoNode) {
const currentTime = videoNode.currentTime;
if (currentTime <= 5) { // play next video if someone hits Next button
if (previousListUri && permanentUrl !== previousListUri) doPlay(previousListUri); if (shouldPlayNextUrl) {
} else { doPlay(nextRecommendedUri);
videoNode.currentTime = 0; // rewind if video is over 5 seconds and they hit the back button
} } else if (videoNode && videoNode.currentTime > 5) {
setDoNavigate(false); 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); setEnded(false);
setPlayNextUrl(true); setPlayNextUrl(true);
}, [ }, [
@ -262,6 +263,7 @@ function VideoViewer(props: Props) {
videoNode, videoNode,
]); ]);
// functionality to run on video end
React.useEffect(() => { React.useEffect(() => {
if (!ended) return; if (!ended) return;
@ -274,13 +276,21 @@ function VideoViewer(props: Props) {
if (embedded) { if (embedded) {
setIsEndedEmbed(true); setIsEndedEmbed(true);
// show autoplay countdown div if not playlist
} else if (!collectionId && autoplayNext) { } else if (!collectionId && autoplayNext) {
setShowAutoplayCountdown(true); setShowAutoplayCountdown(true);
// if a playlist, navigate to next item
} else if (collectionId) { } else if (collectionId) {
setDoNavigate(true); setDoNavigate(true);
} }
clearPosition(uri); 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]); }, [adUrl, autoplayNext, clearPosition, collectionId, embedded, ended, setAdUrl, uri]);
// MORE ON PLAY STUFF // MORE ON PLAY STUFF
@ -300,7 +310,7 @@ function VideoViewer(props: Props) {
analytics.videoIsPlaying(false, player); analytics.videoIsPlaying(false, player);
} }
function onDispose(event, player) { function onPlayerClosed(event, player) {
handlePosition(player); handlePosition(player);
analytics.videoIsPlaying(false, player); analytics.videoIsPlaying(false, player);
} }
@ -328,6 +338,7 @@ function VideoViewer(props: Props) {
}; };
const onPlayerReady = useCallback((player: Player, videoNode: any) => { const onPlayerReady = useCallback((player: Player, videoNode: any) => {
// add buttons and initialize some settings for the player
if (!embedded) { if (!embedded) {
setVideoNode(videoNode); setVideoNode(videoNode);
player.muted(muted); player.muted(muted);
@ -335,6 +346,21 @@ function VideoViewer(props: Props) {
player.playbackRate(videoPlaybackRate); player.playbackRate(videoPlaybackRate);
if (!isMarkdownOrComment) { if (!isMarkdownOrComment) {
addTheaterModeButton(player, toggleVideoTheaterMode); 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) { if (collectionId) {
addPlayNextButton(player, doPlayNext); addPlayNextButton(player, doPlayNext);
addPlayPreviousButton(player, doPlayPrevious); 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 // PR: #5535
// Move the restoration to a later `loadedmetadata` phase to counter the // Move the restoration to a later `loadedmetadata` phase to counter the
// delay from the header-fetch. This is a temp change until the next // delay from the header-fetch. This is a temp change until the next
// re-factoring. // re-factoring.
player.on('loadedmetadata', () => restorePlaybackRate(player)); const restorePlaybackRateEvent = () => restorePlaybackRate(player);
// Override the "auto" algorithm to post-process the result // Override the "auto" algorithm to post-process the result
player.on('loadedmetadata', () => { const overrideAutoAlgorithm = () => {
const vhs = player.tech(true).vhs; const vhs = player.tech(true).vhs;
if (vhs) { if (vhs) {
// https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884 // https://github.com/videojs/http-streaming/issues/749#issuecomment-606972884
vhs.selectPlaylist = lastBandwidthSelector; vhs.selectPlaylist = lastBandwidthSelector;
} }
}); };
player.on('ended', () => setEnded(true)); const onPauseEvent = (event) => onPause(event, player);
player.on('play', onPlay); const onPlayerClosedEvent = (event) => onPlayerClosed(event, player);
player.on('pause', (event) => onPause(event, player)); const onVolumeChange = () => {
player.on('dispose', (event) => onDispose(event, player)); if (player) {
updateVolumeState(player.volume(), player.muted());
player.on('error', () => { }
};
const onPlayerEnded = () => setEnded(true);
const onError = () => {
const error = player.error(); const error = player.error();
if (error) { if (error) {
analytics.sentryError('Video.js error', error); analytics.sentryError('Video.js error', error);
} }
}); };
player.on('volumechange', () => { const onRateChange = () => {
if (player) {
updateVolumeState(player.volume(), player.muted());
}
});
player.on('ratechange', () => {
const HAVE_NOTHING = 0; // https://docs.videojs.com/player#readyState const HAVE_NOTHING = 0; // https://docs.videojs.com/player#readyState
if (player && player.readyState() !== HAVE_NOTHING) { if (player && player.readyState() !== HAVE_NOTHING) {
// The playbackRate occasionally resets to 1, typically when loading a fresh video or when 'src' changes. // 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. // [ ] Ideally, the controlBar should be hidden to prevent users from changing the rate while loading.
setVideoPlaybackRate(player.playbackRate()); setVideoPlaybackRate(player.playbackRate());
} }
}); };
if (position && !isLivestreamClaim) { const moveToPosition = () => {
player.currentTime(position); // 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); Chapters.parseAndLoad(player, claim);

View file

@ -331,7 +331,7 @@ $control-bar-popup-font-size: 0.8rem;
// TODO: make sure there's no bad side effects of this // TODO: make sure there's no bad side effects of this
button.vjs-big-play-button { button.vjs-big-play-button {
display: none !important; display: none;
} }
.vjs-big-play-centered { .vjs-big-play-centered {
@ -465,3 +465,16 @@ button.vjs-big-play-button {
background-color: var(--color-error); background-color: var(--color-error);
width: 2px; 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;
}

View file

@ -7374,6 +7374,7 @@ flatten@^1.0.2:
flow-bin@^0.97.0: flow-bin@^0.97.0:
version "0.97.0" version "0.97.0"
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.97.0.tgz#036ffcfc27503367a9d906ec9d843a0aa6f6bb83" 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: flow-typed@^2.3.0:
version "2.6.2" version "2.6.2"