From be4b8a4631aa846ff85772eae092b745a0423f90 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 8 Nov 2021 21:22:49 +0100 Subject: [PATCH] coming along well --- .../videoViewer/internal/videojs-events.jsx | 405 +++++++++--------- .../internal/videojs-functions.jsx | 56 +++ .../viewers/videoViewer/internal/videojs.jsx | 71 +-- 3 files changed, 277 insertions(+), 255 deletions(-) create mode 100644 ui/component/viewers/videoViewer/internal/videojs-functions.jsx diff --git a/ui/component/viewers/videoViewer/internal/videojs-events.jsx b/ui/component/viewers/videoViewer/internal/videojs-events.jsx index 29db3ec50..ae21ddd69 100644 --- a/ui/component/viewers/videoViewer/internal/videojs-events.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs-events.jsx @@ -1,196 +1,209 @@ -// // Add various event listeners to player -// player.one('play', onInitialPlay); -// player.on('play', resolveCtrlText); -// player.on('pause', resolveCtrlText); -// player.on('loadstart', resolveCtrlText); -// player.on('fullscreenchange', resolveCtrlText); -// player.on('volumechange', resolveCtrlText); -// player.on('volumechange', onVolumeChange); -// player.on('error', onError); -// player.on('ended', onEnded); -// -// const tapToUnmuteRef = useRef(); -// const tapToRetryRef = useRef(); -// -// const TAP = { -// NONE: 'NONE', -// UNMUTE: 'UNMUTE', -// RETRY: 'RETRY', -// }; -// -// function showTapButton(tapButton) { -// const setButtonVisibility = (theRef, newState) => { -// // Use the DOM to control the state of the button to prevent re-renders. -// if (theRef.current) { -// const curState = theRef.current.style.visibility === 'visible'; -// if (newState !== curState) { -// theRef.current.style.visibility = newState ? 'visible' : 'hidden'; -// } -// } -// }; -// -// switch (tapButton) { -// case TAP.NONE: -// setButtonVisibility(/*tapToUnmuteRef*/, false); -// setButtonVisibility(tapToRetryRef, false); -// break; -// case TAP.UNMUTE: -// setButtonVisibility(tapToUnmuteRef, true); -// setButtonVisibility(tapToRetryRef, false); -// break; -// case TAP.RETRY: -// setButtonVisibility(tapToUnmuteRef, false); -// setButtonVisibility(tapToRetryRef, true); -// break; -// default: -// if (isDev) throw new Error('showTapButton: unexpected ref'); -// break; -// } -// } -// -// function unmuteAndHideHint() { -// const player = playerRef.current; -// if (player) { -// player.muted(false); -// if (player.volume() === 0) { -// player.volume(1.0); -// } -// } -// showTapButton(TAP.NONE); -// } -// -// function retryVideoAfterFailure() { -// const player = playerRef.current; -// if (player) { -// setReload(Date.now()); -// showTapButton(TAP.NONE); -// } -// } -// -// function resolveCtrlText(e) { -// // Override the player's control text. We override to: -// // 1. Add keyboard shortcut to the tool-tip. -// // 2. Override videojs' i18n and use our own (don't want to have 2 systems). -// // -// // Notes: -// // - For dynamic controls (e.g. play/pause), those unfortunately need to be -// // updated again at their event-listener level (that's just the way videojs -// // updates the text), hence the need to listen to 'play', 'pause' and 'volumechange' -// // on top of just 'loadstart'. -// // - videojs changes the MuteToggle text at 'loadstart', so this was chosen -// // as the listener to update static texts. -// -// const setLabel = (controlBar, childName, label) => { -// const c = controlBar.getChild(childName); -// if (c) { -// c.controlText(label); -// } -// }; -// -// const player = playerRef.current; -// if (player) { -// const ctrlBar = player.getChild('controlBar'); -// switch (e.type) { -// case 'play': -// setLabel(ctrlBar, 'PlayToggle', __('Pause (space)')); -// break; -// case 'pause': -// setLabel(ctrlBar, 'PlayToggle', __('Play (space)')); -// break; -// case 'volumechange': -// ctrlBar -// .getChild('VolumePanel') -// .getChild('MuteToggle') -// .controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)')); -// break; -// case 'fullscreenchange': -// setLabel( -// ctrlBar, -// 'FullscreenToggle', -// player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)') -// ); -// break; -// case 'loadstart': -// // --- Do everything --- -// setLabel(ctrlBar, 'PlaybackRateMenuButton', __('Playback Rate (<, >)')); -// setLabel(ctrlBar, 'QualityButton', __('Quality')); -// setLabel(ctrlBar, 'PlayNextButton', __('Play Next (SHIFT+N)')); -// setLabel(ctrlBar, 'PlayPreviousButton', __('Play Previous (SHIFT+P)')); -// setLabel(ctrlBar, 'TheaterModeButton', videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)')); -// setLabel(ctrlBar, 'AutoplayNextButton', autoplaySetting ? __('Autoplay Next On') : __('Autoplay Next Off')); -// -// resolveCtrlText({ type: 'play' }); -// resolveCtrlText({ type: 'pause' }); -// resolveCtrlText({ type: 'volumechange' }); -// resolveCtrlText({ type: 'fullscreenchange' }); -// break; -// default: -// if (isDev) throw Error('Unexpected: ' + e.type); -// break; -// } -// } -// } -// -// function onInitialPlay() { -// const player = playerRef.current; -// if (player && (player.muted() || player.volume() === 0)) { -// // The css starts as "hidden". We make it visible here without -// // re-rendering the whole thing. -// showTapButton(TAP.UNMUTE); -// } else { -// showTapButton(TAP.NONE); -// } -// } -// -// function onVolumeChange() { -// const player = playerRef.current; -// if (player && !player.muted()) { -// showTapButton(TAP.NONE); -// } -// } -// -// function onError() { -// const player = playerRef.current; -// showTapButton(TAP.RETRY); -// -// // reattach initial play listener in case we recover from error successfully -// // $FlowFixMe -// player.one('play', onInitialPlay); -// -// if (player && player.loadingSpinner) { -// player.loadingSpinner.hide(); -// } -// } -// -// const onEnded = React.useCallback(() => { -// if (!adUrl) { -// showTapButton(TAP.NONE); -// } -// }, [adUrl]); -// -// useEffect(() => { -// const player = playerRef.current; -// if (player) { -// const controlBar = player.getChild('controlBar'); -// controlBar -// .getChild('TheaterModeButton') -// .controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)')); -// } -// }, [videoTheaterMode]); -// -// useEffect(() => { -// const player = playerRef.current; -// if (player) { -// const touchOverlay = player.getChild('TouchOverlay'); -// const controlBar = player.getChild('controlBar') || touchOverlay.getChild('controlBar'); -// const autoplayButton = controlBar.getChild('AutoplayNextButton'); -// -// if (autoplayButton) { -// const title = autoplaySetting ? __('Autoplay Next On') : __('Autoplay Next Off'); -// -// autoplayButton.controlText(title); -// autoplayButton.setAttribute('aria-label', title); -// autoplayButton.setAttribute('aria-checked', autoplaySetting); -// } -// } -// }, [autoplaySetting]); +// @flow +import React, { useEffect, useRef, useState } from 'react'; + +const isDev = process.env.NODE_ENV !== 'production'; + +const TAP = { + NONE: 'NONE', + UNMUTE: 'UNMUTE', + RETRY: 'RETRY', +}; + +export default ({ tapToUnmuteRef, tapToRetryRef, setReload, }) => { + function resolveCtrlText(e) { + // Override the player's control text. We override to: + // 1. Add keyboard shortcut to the tool-tip. + // 2. Override videojs' i18n and use our own (don't want to have 2 systems). + // + // Notes: + // - For dynamic controls (e.g. play/pause), those unfortunately need to be + // updated again at their event-listener level (that's just the way videojs + // updates the text), hence the need to listen to 'play', 'pause' and 'volumechange' + // on top of just 'loadstart'. + // - videojs changes the MuteToggle text at 'loadstart', so this was chosen + // as the listener to update static texts. + + const setLabel = (controlBar, childName, label) => { + const c = controlBar.getChild(childName); + if (c) { + c.controlText(label); + } + }; + + const player = playerRef.current; + if (player) { + const ctrlBar = player.getChild('controlBar'); + switch (e.type) { + case 'play': + setLabel(ctrlBar, 'PlayToggle', __('Pause (space)')); + break; + case 'pause': + setLabel(ctrlBar, 'PlayToggle', __('Play (space)')); + break; + case 'volumechange': + ctrlBar + .getChild('VolumePanel') + .getChild('MuteToggle') + .controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)')); + break; + case 'fullscreenchange': + setLabel( + ctrlBar, + 'FullscreenToggle', + player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)') + ); + break; + case 'loadstart': + // --- Do everything --- + setLabel(ctrlBar, 'PlaybackRateMenuButton', __('Playback Rate (<, >)')); + setLabel(ctrlBar, 'QualityButton', __('Quality')); + setLabel(ctrlBar, 'PlayNextButton', __('Play Next (SHIFT+N)')); + setLabel(ctrlBar, 'PlayPreviousButton', __('Play Previous (SHIFT+P)')); + setLabel(ctrlBar, 'TheaterModeButton', videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)')); + setLabel(ctrlBar, 'AutoplayNextButton', autoplaySetting ? __('Autoplay Next On') : __('Autoplay Next Off')); + + resolveCtrlText({ type: 'play' }); + resolveCtrlText({ type: 'pause' }); + resolveCtrlText({ type: 'volumechange' }); + resolveCtrlText({ type: 'fullscreenchange' }); + break; + default: + if (isDev) throw Error('Unexpected: ' + e.type); + break; + } + } + } + + function onInitialPlay() { + const player = playerRef.current; + if (player && (player.muted() || player.volume() === 0)) { + // The css starts as "hidden". We make it visible here without + // re-rendering the whole thing. + showTapButton(TAP.UNMUTE); + } else { + showTapButton(TAP.NONE); + } + } + + function onVolumeChange() { + const player = playerRef.current; + if (player && !player.muted()) { + showTapButton(TAP.NONE); + } + } + + function onError() { + const player = playerRef.current; + showTapButton(TAP.RETRY); + + // reattach initial play listener in case we recover from error successfully + // $FlowFixMe + player.one('play', onInitialPlay); + + if (player && player.loadingSpinner) { + player.loadingSpinner.hide(); + } + } + + // const onEnded = React.useCallback(() => { + // if (!adUrl) { + // showTapButton(TAP.NONE); + // } + // }, [adUrl]); + + useEffect(() => { + const player = playerRef.current; + if (player) { + const controlBar = player.getChild('controlBar'); + controlBar + .getChild('TheaterModeButton') + .controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)')); + } + }, [videoTheaterMode]); + + + function unmuteAndHideHint() { + const player = playerRef.current; + if (player) { + player.muted(false); + if (player.volume() === 0) { + player.volume(1.0); + } + } + showTapButton(TAP.NONE); + } + + function retryVideoAfterFailure() { + const player = playerRef.current; + if (player) { + setReload(Date.now()); + showTapButton(TAP.NONE); + } + } + + function showTapButton(tapButton) { + const setButtonVisibility = (theRef, newState) => { + // Use the DOM to control the state of the button to prevent re-renders. + if (theRef.current) { + const curState = theRef.current.style.visibility === 'visible'; + if (newState !== curState) { + theRef.current.style.visibility = newState ? 'visible' : 'hidden'; + } + } + }; + + switch (tapButton) { + case TAP.NONE: + setButtonVisibility(/*tapToUnmuteRef*/, false); + setButtonVisibility(tapToRetryRef, false); + break; + case TAP.UNMUTE: + setButtonVisibility(tapToUnmuteRef, true); + setButtonVisibility(tapToRetryRef, false); + break; + case TAP.RETRY: + setButtonVisibility(tapToUnmuteRef, false); + setButtonVisibility(tapToRetryRef, true); + break; + default: + if (isDev) throw new Error('showTapButton: unexpected ref'); + break; + } + } + + + useEffect(() => { + const player = playerRef.current; + if (player) { + const touchOverlay = player.getChild('TouchOverlay'); + const controlBar = player.getChild('controlBar') || touchOverlay.getChild('controlBar'); + const autoplayButton = controlBar.getChild('AutoplayNextButton'); + + if (autoplayButton) { + const title = autoplaySetting ? __('Autoplay Next On') : __('Autoplay Next Off'); + + autoplayButton.controlText(title); + autoplayButton.setAttribute('aria-label', title); + autoplayButton.setAttribute('aria-checked', autoplaySetting); + } + } + }, [autoplaySetting]); + + + // Add various event listeners to player + player.one('play', onInitialPlay); + player.on('play', resolveCtrlText); + player.on('pause', resolveCtrlText); + player.on('loadstart', resolveCtrlText); + player.on('fullscreenchange', resolveCtrlText); + player.on('volumechange', resolveCtrlText); + player.on('volumechange', onVolumeChange); + player.on('error', onError); + player.on('ended', onEnded); + + + return { + detectFileType, + createVideoPlayerDOM, + }; +}; diff --git a/ui/component/viewers/videoViewer/internal/videojs-functions.jsx b/ui/component/viewers/videoViewer/internal/videojs-functions.jsx new file mode 100644 index 000000000..021648fe9 --- /dev/null +++ b/ui/component/viewers/videoViewer/internal/videojs-functions.jsx @@ -0,0 +1,56 @@ +export default ({ source, sourceType, videoJsOptions, isAudio }) => { + + function detectFileType() { + return new Promise(async (res, rej) => { + try { + const response = await fetch(source, { method: 'HEAD', cache: 'no-store' }); + + // Temp variables to hold results + let finalType = sourceType; + let finalSource = source; + + // override type if we receive an .m3u8 (transcoded mp4) + // do we need to check if explicitly redirected + // or is checking extension only a safer method + if (response && response.redirected && response.url && response.url.endsWith('m3u8')) { + finalType = 'application/x-mpegURL'; + finalSource = response.url; + } + + // Modify video source in options + videoJsOptions.sources = [ + { + src: finalSource, + type: finalType, + }, + ]; + + return res(videoJsOptions); + } catch (error) { + return rej(error); + } + }); + } + + // TODO: can remove this function as well + // Create the video DOM element and wrapper + function createVideoPlayerDOM(container) { + if (!container) return; + + // 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'); + el.className = 'video-js vjs-big-play-centered '; + wrapper.appendChild(el); + + container.appendChild(wrapper); + + return el; + } + + return { + detectFileType, + createVideoPlayerDOM, + }; +}; diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index cc1ed701f..f0de188da 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -18,6 +18,7 @@ import runAds from './ads'; import LbryVolumeBarClass from './lbry-volume-bar'; import keyboardShorcuts from './videojs-keyboard-shortcuts'; import events from './videojs-events'; +import functions from './videojs-functions'; export type Player = { on: (string, (any) => void) => void, @@ -134,16 +135,13 @@ export default React.memo(function VideoJs(props: Props) { playPrevious, } = props; - const { curried_function } = keyboardShorcuts({ - toggleVideoTheaterMode, - playNext, - playPrevious, - }); - - // const { initializeEvents, unmuteAndHideHint, retryVideoAfterFailure } = events(); - + // initiate keyboard shortcuts + const { curried_function } = keyboardShorcuts({ toggleVideoTheaterMode, playNext, playPrevious }); const [reload, setReload] = useState('initial'); + + const { initializeEvents, unmuteAndHideHint, retryVideoAfterFailure } = events(videoTheaterMode, setReload, autoplaySetting); + // will later store the videojs player const playerRef = useRef(); const containerRef = useRef(); @@ -173,53 +171,7 @@ export default React.memo(function VideoJs(props: Props) { }, }; - // Create the video DOM element and wrapper - function createVideoPlayerDOM(container) { - if (!container) return; - - // 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'); - el.className = 'video-js vjs-big-play-centered '; - wrapper.appendChild(el); - - container.appendChild(wrapper); - - return el; - } - - function detectFileType() { - return new Promise(async (res, rej) => { - try { - const response = await fetch(source, { method: 'HEAD', cache: 'no-store' }); - - // Temp variables to hold results - let finalType = sourceType; - let finalSource = source; - - // override type if we receive an .m3u8 (transcoded mp4) - // do we need to check if explicitly redirected - // or is checking extension only a safer method - if (response && response.redirected && response.url && response.url.endsWith('m3u8')) { - finalType = 'application/x-mpegURL'; - finalSource = response.url; - } - - // Modify video source in options - videoJsOptions.sources = [ - { - src: finalSource, - type: finalType, - }, - ]; - - return res(videoJsOptions); - } catch (error) { - return rej(error); - } - }); - } + const { detectFileType, createVideoPlayerDOM } = functions({ source, sourceType, videoJsOptions, isAudio }); // Initialize video.js function initializeVideoPlayer(el) { @@ -233,7 +185,7 @@ export default React.memo(function VideoJs(props: Props) { runAds(internalFeatureEnabled, allowPreRoll, player); - // initializeEvents(player, tapToRetryRef, tapToUnmuteRef); + initializeEvents({ player, tapToRetryRef, tapToUnmuteRef }); // Replace volume bar with custom LBRY volume bar LbryVolumeBarClass.replaceExisting(player); @@ -259,7 +211,6 @@ export default React.memo(function VideoJs(props: Props) { } // set playsinline for mobile - // TODO: make this better player.children_[0].setAttribute('playsinline', ''); // I think this is a callback function @@ -275,6 +226,7 @@ export default React.memo(function VideoJs(props: Props) { return vjs; } + // todo: what does this do exactly? useEffect(() => { const player = playerRef.current; if (replay && player) { @@ -282,6 +234,7 @@ export default React.memo(function VideoJs(props: Props) { } }, [replay]); + /** 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. useEffect(() => { const vjsElement = createVideoPlayerDOM(containerRef.current); @@ -354,7 +307,7 @@ export default React.memo(function VideoJs(props: Props) { button="link" icon={ICONS.VOLUME_MUTED} className="video-js--tap-to-unmute" - // onClick={unmuteAndHideHint} + onClick={unmuteAndHideHint} ref={tapToUnmuteRef} />