From 8c9d905a913c907785fb48d201d3fca472afbcd2 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 24 Jan 2023 18:12:37 +0100 Subject: [PATCH] quality display control, source selection beginning, mobile player UI, keyboard shortcuts Co-authored-by: Jip Frijlink Co-authored-by: James Hawkins --- index.html | 2 +- src/components/Icon.tsx | 2 + src/components/video/DecoratedVideoPlayer.tsx | 115 ++++++++--- src/components/video/VideoContext.tsx | 12 +- src/components/video/VideoPlayer.tsx | 84 +++++++- .../video/controls/MobileCenterControl.tsx | 20 ++ .../video/controls/PauseControl.tsx | 2 + .../video/controls/ProgressControl.tsx | 2 +- .../video/controls/QualityDisplayControl.tsx | 14 ++ src/components/video/controls/SkipTime.tsx | 3 +- .../video/controls/SourceControl.tsx | 4 +- .../video/controls/SourceSelectionControl.tsx | 185 ++++++++++++++++++ src/components/video/controls/TimeControl.tsx | 38 ++-- .../video/controls/VolumeControl.tsx | 12 +- src/components/video/hooks/useVideoPlayer.ts | 17 ++ .../video/parts/VideoPlayerHeader.tsx | 16 +- .../video/parts/VideoPlayerIconButton.tsx | 3 +- src/components/video/parts/VideoPopout.tsx | 43 ++-- src/hooks/useIsMobile.ts | 28 +++ src/hooks/useProgressBar.ts | 6 +- src/hooks/useVolumeToggle.ts | 22 +++ src/index.tsx | 6 +- src/setup/index.css | 13 ++ src/views/media/MediaView.tsx | 3 +- 24 files changed, 556 insertions(+), 96 deletions(-) create mode 100644 src/components/video/controls/MobileCenterControl.tsx create mode 100644 src/components/video/controls/QualityDisplayControl.tsx create mode 100644 src/components/video/controls/SourceSelectionControl.tsx create mode 100644 src/hooks/useIsMobile.ts create mode 100644 src/hooks/useVolumeToggle.ts diff --git a/index.html b/index.html index cd808f2e..5e3d67e5 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ - + = { episodes: ``, skip_forward: ``, skip_backward: ``, + file: ``, }; export const Icon = memo((props: IconProps) => { diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index e34f51c0..700b0149 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -1,4 +1,5 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; +import { useIsMobile } from "@/hooks/useIsMobile"; import { useCallback, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { AirplayControl } from "./controls/AirplayControl"; @@ -7,21 +8,24 @@ import { ChromeCastControl } from "./controls/ChromeCastControl"; import { FullscreenControl } from "./controls/FullscreenControl"; import { LoadingControl } from "./controls/LoadingControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl"; +import { MobileCenterControl } from "./controls/MobileCenterControl"; +import { PageTitleControl } from "./controls/PageTitleControl"; import { PauseControl } from "./controls/PauseControl"; import { ProgressControl } from "./controls/ProgressControl"; +import { QualityDisplayControl } from "./controls/QualityDisplayControl"; import { SeriesSelectionControl } from "./controls/SeriesSelectionControl"; import { ShowTitleControl } from "./controls/ShowTitleControl"; import { SkipTime } from "./controls/SkipTime"; +import { SourceSelectionControl } from "./controls/SourceSelectionControl"; import { TimeControl } from "./controls/TimeControl"; import { VolumeControl } from "./controls/VolumeControl"; -import { PageTitleControl } from "./controls/PageTitleControl"; import { VideoPlayerError } from "./parts/VideoPlayerError"; import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; interface DecoratedVideoPlayerProps { - media?: MWMediaMeta; + media?: DetailedMeta; onGoBack?: () => void; } @@ -56,8 +60,10 @@ export function DecoratedVideoPlayer( props: VideoPlayerProps & DecoratedVideoPlayerProps ) { const top = useRef(null); + const center = useRef(null); const bottom = useRef(null); const [show, setShow] = useState(false); + const { isMobile } = useIsMobile(); const onBackdropChange = useCallback( (showing: boolean) => { @@ -68,8 +74,8 @@ export function DecoratedVideoPlayer( return ( - - + +
@@ -77,34 +83,29 @@ export function DecoratedVideoPlayer(
- -
- -
- -
- - - - +
+
-
- + + ) : ( + "" + )} - + +
+ + +
+
+ {isMobile && } + +
+
+ {isMobile ? ( +
+
+
+ + +
+ +
+ ) : ( + <> + +
+ + + + + + + + )} +
diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index 8cbc8e4a..10941206 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -1,4 +1,4 @@ -import { MWStreamType } from "@/backend/helpers/streams"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; import React, { createContext, MutableRefObject, @@ -15,16 +15,23 @@ import { interface VideoPlayerContextType { source: string | null; sourceType: MWStreamType; + quality: MWStreamQuality; state: PlayerContext; } const initial: VideoPlayerContextType = { source: null, sourceType: MWStreamType.MP4, + quality: MWStreamQuality.QUNKNOWN, state: initialPlayerState, }; type VideoPlayerContextAction = - | { type: "SET_SOURCE"; url: string; sourceType: MWStreamType } + | { + type: "SET_SOURCE"; + url: string; + sourceType: MWStreamType; + quality: MWStreamQuality; + } | { type: "UPDATE_PLAYER"; state: PlayerContext; @@ -38,6 +45,7 @@ function videoPlayerContextReducer( if (action.type === "SET_SOURCE") { video.source = action.url; video.sourceType = action.sourceType; + video.quality = action.quality; return video; } if (action.type === "UPDATE_PLAYER") { diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index d00b917a..8f33e11e 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,7 +1,12 @@ import { useGoBack } from "@/hooks/useGoBack"; +import { useVolumeControl } from "@/hooks/useVolumeToggle"; import { forwardRef, useContext, useEffect, useRef } from "react"; import { VideoErrorBoundary } from "./parts/VideoErrorBoundary"; -import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; +import { + useVideoPlayerState, + VideoPlayerContext, + VideoPlayerContextProvider, +} from "./VideoContext"; export interface VideoPlayerProps { autoPlay?: boolean; @@ -13,15 +18,83 @@ const VideoPlayerInternals = forwardRef< { autoPlay: boolean } >((props, ref) => { const video = useContext(VideoPlayerContext); - const didInitialize = useRef(null); + const didInitialize = useRef<{ source: string | null } | null>(null); + const { videoState } = useVideoPlayerState(); + const { toggleVolume } = useVolumeControl(); useEffect(() => { - if (didInitialize.current) return; + const value = { source: video.source }; + const hasChanged = value.source !== didInitialize.current?.source; + if (!hasChanged) return; if (!video.state.hasInitialized || !video.source) return; video.state.initPlayer(video.source, video.sourceType); - didInitialize.current = true; + didInitialize.current = value; }, [didInitialize, video]); + useEffect(() => { + let isRolling = false; + const onKeyDown = (evt: KeyboardEvent) => { + if (!videoState.isFocused) return; + if (!ref || !(ref as any)?.current) return; + const el = (ref as any).current as HTMLVideoElement; + + switch (evt.key.toLowerCase()) { + // Toggle fullscreen + case "f": + if (videoState.isFullscreen) { + videoState.exitFullscreen(); + } else { + videoState.enterFullscreen(); + } + break; + + // Skip backwards + case "arrowleft": + videoState.setTime(videoState.time - 5); + break; + + // Skip forward + case "arrowright": + videoState.setTime(videoState.time + 5); + break; + + // Pause / play + case " ": + if (videoState.isPaused) { + videoState.play(); + } else { + videoState.pause(); + } + break; + + // Mute + case "m": + toggleVolume(); + break; + + // Do a barrel Roll! + case "r": + if (isRolling) return; + isRolling = true; + el.classList.add("roll"); + setTimeout(() => { + isRolling = false; + el.classList.remove("roll"); + }, 1000); + break; + + default: + break; + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [videoState, toggleVolume, ref]); + // muted attribute is required for safari, as they cant change the volume itself return (