diff --git a/ui/component/viewers/videoViewer/internal/videojs-keyboard-shortcuts.jsx b/ui/component/viewers/videoViewer/internal/videojs-shortcuts.jsx similarity index 71% rename from ui/component/viewers/videoViewer/internal/videojs-keyboard-shortcuts.jsx rename to ui/component/viewers/videoViewer/internal/videojs-shortcuts.jsx index 0791f2883..3e613e118 100644 --- a/ui/component/viewers/videoViewer/internal/videojs-keyboard-shortcuts.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs-shortcuts.jsx @@ -6,6 +6,8 @@ import isUserTyping from 'util/detect-typing'; const SEEK_STEP_5 = 5; const SEEK_STEP = 10; // time to seek in seconds +const VOLUME_CHANGE_ON_SCROLL = 0.05; +const PRECISE_VOLUME_CHANGE_ON_SCROLL = 0.01; // check if active (clicked) element is part of video div, used for keyboard shortcuts (volume etc) function activeElementIsPartOfVideoElement() { @@ -14,24 +16,24 @@ function activeElementIsPartOfVideoElement() { return videoElementParent.contains(activeElement); } -function volumeUp(event, playerRef) { +function volumeUp(event, playerRef, checkIsActive = true, amount = 0.05) { // dont run if video element is not active element (otherwise runs when scrolling using keypad) const videoElementIsActive = activeElementIsPartOfVideoElement(); const player = playerRef.current; - if (!player || !videoElementIsActive) return; + if (!player || (checkIsActive && !videoElementIsActive)) return; event.preventDefault(); - player.volume(player.volume() + 0.05); + player.volume(player.volume() + amount); OVERLAY.showVolumeverlay(player, Math.round(player.volume() * 100)); player.userActive(true); } -function volumeDown(event, playerRef) { +function volumeDown(event, playerRef, checkIsActive = true, amount = 0.05) { // dont run if video element is not active element (otherwise runs when scrolling using keypad) const videoElementIsActive = activeElementIsPartOfVideoElement(); const player = playerRef.current; - if (!player || !videoElementIsActive) return; + if (!player || (checkIsActive && !videoElementIsActive)) return; event.preventDefault(); - player.volume(player.volume() - 0.05); + player.volume(player.volume() - amount); OVERLAY.showVolumeverlay(player, Math.round(player.volume() * 100)); player.userActive(true); } @@ -94,7 +96,7 @@ function changePlaybackSpeed(shouldSpeedUp: boolean, playerRef) { } } -const VideoJsKeyboardShorcuts = ({ +const VideoJsShorcuts = ({ playNext, playPrevious, toggleVideoTheaterMode, @@ -163,15 +165,65 @@ const VideoJsKeyboardShorcuts = ({ if (e.keyCode === KEYCODES.NINE) seekVideo(90 / 100, playerRef, containerRef, true); } - var curried_function = function (playerRef: any, containerRef: any) { + const handleVideoScrollWheel = (event, playerRef, containerRef) => { + // Handle precise volume control when scrolling over the video player while holding down the "SHIFT"-key + const player = playerRef.current; + const videoNode = containerRef.current && containerRef.current.querySelector('video'); + + if (!videoNode || !player || isUserTyping() || !event.shiftKey) return; + + event.preventDefault(); + + const delta = event.deltaY; + + if (delta > 0) { + volumeDown(event, playerRef, false, PRECISE_VOLUME_CHANGE_ON_SCROLL); + } else if (delta < 0) { + volumeUp(event, playerRef, false, PRECISE_VOLUME_CHANGE_ON_SCROLL); + } + }; + + const handleVolumeBarScrollWheel = (event, volumeElement, playerRef, containerRef) => { + // Handle generic and precise volume control when scrolling over the volume bar + const player = playerRef.current; + const videoNode = containerRef.current && containerRef.current.querySelector('video'); + + if (!volumeElement || !player || !videoNode || isUserTyping()) return; + + event.preventDefault(); + event.stopImmediatePropagation(); + + const delta = event.deltaY; + const changeAmount = event.shiftKey ? PRECISE_VOLUME_CHANGE_ON_SCROLL : VOLUME_CHANGE_ON_SCROLL; + + if (delta > 0) { + volumeDown(event, playerRef, false, changeAmount); + } else if (delta < 0) { + volumeUp(event, playerRef, false, changeAmount); + } + }; + + const createKeyDownShortcutsHandler = function (playerRef: any, containerRef: any) { return function curried_func(e: any) { handleKeyDown(e, playerRef, containerRef); }; }; + const createVideoScrollShortcutsHandler = function (playerRef: any, containerRef: any) { + return function curried_func(e: any) { + handleVideoScrollWheel(e, playerRef, containerRef); + }; + }; + const createVolumePanelScrollShortcutsHandler = function (volumeElement: any, playerRef: any, containerRef: any) { + return function curried_func(e: any) { + handleVolumeBarScrollWheel(e, volumeElement, playerRef, containerRef); + }; + }; return { - curried_function, + createKeyDownShortcutsHandler, + createVideoScrollShortcutsHandler, + createVolumePanelScrollShortcutsHandler, }; }; -export default VideoJsKeyboardShorcuts; +export default VideoJsShorcuts; diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 49926b6fa..a3df0f560 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -15,7 +15,7 @@ import events from './videojs-events'; import eventTracking from 'videojs-event-tracking'; import functions from './videojs-functions'; import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin'; -import keyboardShorcuts from './videojs-keyboard-shortcuts'; +import keyboardShorcuts from './videojs-shortcuts'; import LbryVolumeBarClass from './lbry-volume-bar'; import Chromecast from './chromecast'; import playerjs from 'player.js'; @@ -103,6 +103,8 @@ 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(); const IS_MOBILE = platform.isMobile(); @@ -176,13 +178,22 @@ export default React.memo(function VideoJs(props: Props) { const tapToUnmuteRef = useRef(); const tapToRetryRef = useRef(); const playerServerRef = useRef(); + const volumePanelRef = useRef(); + + const keyDownHandlerRef = useRef(); + const videoScrollHandlerRef = useRef(); + const volumePanelScrollHandlerRef = useRef(); const { url: livestreamVideoUrl } = activeLivestreamForChannel || {}; const overrideNativeVhs = !platform.isIPhone(); const showQualitySelector = (!isLivestreamClaim && overrideNativeVhs) || livestreamVideoUrl; // initiate keyboard shortcuts - const { curried_function } = keyboardShorcuts({ + const { + createKeyDownShortcutsHandler, + createVideoScrollShortcutsHandler, + createVolumePanelScrollShortcutsHandler, + } = keyboardShorcuts({ isMobile, isLivestreamClaim, toggleVideoTheaterMode, @@ -370,7 +381,23 @@ export default React.memo(function VideoJs(props: Props) { // Set reference in component state playerRef.current = vjsPlayer; - window.addEventListener('keydown', curried_function(playerRef, containerRef)); + // 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(); + + const keyDownHandler = createKeyDownShortcutsHandler(playerRef, containerRef); + const videoScrollHandler = createVideoScrollShortcutsHandler(playerRef, containerRef); + const volumePanelHandler = createVolumePanelScrollShortcutsHandler(volumePanelRef, playerRef, containerRef); + window.addEventListener('keydown', keyDownHandler); + const containerDiv = containerRef.current; + containerDiv && containerDiv.addEventListener('wheel', videoScrollHandler); + if (volumePanelRef.current) volumePanelRef.current.addEventListener('wheel', volumePanelHandler); + + keyDownHandlerRef.current = keyDownHandler; + videoScrollHandlerRef.current = videoScrollHandler; + volumePanelScrollHandlerRef.current = volumePanelHandler; const controlBar = document.querySelector('.vjs-control-bar'); if (controlBar) { @@ -441,7 +468,14 @@ export default React.memo(function VideoJs(props: Props) { // Cleanup return () => { - window.removeEventListener('keydown', curried_function); + window.removeEventListener('keydown', keyDownHandlerRef.current); + const containerDiv = containerRef.current; + // $FlowFixMe + containerDiv && containerDiv.removeEventListener('wheel', videoScrollHandlerRef.current); + + if (volumePanelRef.current) { + volumePanelRef.current.removeEventListener('wheel', volumePanelScrollHandlerRef.current); + } const player = playerRef.current; if (player) {