From 6ca3196b753705544daf0530605cd13e15525a50 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Thu, 2 Feb 2023 22:04:58 +0100 Subject: [PATCH] the start detaching video state from react Co-authored-by: James Hawkins --- src/video/components/VideoPlayerBase.tsx | 19 ++++++++ .../__old}/DecoratedVideoPlayer.tsx | 0 .../components/__old}/VideoContext.tsx | 0 .../components/__old}/VideoPlayer.tsx | 4 +- .../__old}/controls/AirplayControl.tsx | 0 .../__old}/controls/BackdropControl.tsx | 0 .../__old}/controls/ChromeCastControl.tsx | 0 .../__old}/controls/FullscreenControl.tsx | 0 .../__old}/controls/LoadingControl.tsx | 0 .../__old}/controls/MiddlePauseControl.tsx | 0 .../__old}/controls/MobileCenterControl.tsx | 0 .../__old}/controls/PageTitleControl.tsx | 0 .../__old}/controls/PauseControl.tsx | 0 .../__old}/controls/ProgressControl.tsx | 0 .../controls/ProgressListenerControl.tsx | 0 .../__old}/controls/QualityDisplayControl.tsx | 0 .../controls/SeriesSelectionControl.tsx | 0 .../__old}/controls/ShowControl.tsx | 0 .../__old}/controls/ShowTitleControl.tsx | 0 .../components/__old}/controls/SkipTime.tsx | 0 .../__old}/controls/SourceControl.tsx | 0 .../controls/SourceSelectionControl.tsx | 0 .../__old}/controls/TimeControl.tsx | 0 .../__old}/controls/VolumeControl.tsx | 0 .../components/__old}/hooks/controlVideo.ts | 0 .../hooks/useCurrentSeriesEpisodeInfo.ts | 0 .../components/__old}/hooks/useVideoPlayer.ts | 0 .../components/__old}/hooks/utils.ts | 0 .../components/__old}/hooks/volumeStore.ts | 0 .../__old}/parts/VideoErrorBoundary.tsx | 0 .../__old}/parts/VideoPlayerError.tsx | 0 .../__old}/parts/VideoPlayerHeader.tsx | 0 .../__old}/parts/VideoPlayerIconButton.tsx | 0 .../components/__old}/parts/VideoPopout.tsx | 0 src/video/state/cache.ts | 9 ++++ src/video/state/events.ts | 28 ++++++++++++ src/video/state/hooks.tsx | 36 +++++++++++++++ src/video/state/init.ts | 44 +++++++++++++++++++ src/video/state/providers/providerTypes.ts | 7 +++ .../state/providers/videoStateProvider.ts | 40 +++++++++++++++++ src/video/state/types.ts | 36 +++++++++++++++ src/views/media/MediaView.tsx | 2 +- 42 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 src/video/components/VideoPlayerBase.tsx rename src/{components/video => video/components/__old}/DecoratedVideoPlayer.tsx (100%) rename src/{components/video => video/components/__old}/VideoContext.tsx (100%) rename src/{components/video => video/components/__old}/VideoPlayer.tsx (96%) rename src/{components/video => video/components/__old}/controls/AirplayControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/BackdropControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/ChromeCastControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/FullscreenControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/LoadingControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/MiddlePauseControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/MobileCenterControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/PageTitleControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/PauseControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/ProgressControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/ProgressListenerControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/QualityDisplayControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/SeriesSelectionControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/ShowControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/ShowTitleControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/SkipTime.tsx (100%) rename src/{components/video => video/components/__old}/controls/SourceControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/SourceSelectionControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/TimeControl.tsx (100%) rename src/{components/video => video/components/__old}/controls/VolumeControl.tsx (100%) rename src/{components/video => video/components/__old}/hooks/controlVideo.ts (100%) rename src/{components/video => video/components/__old}/hooks/useCurrentSeriesEpisodeInfo.ts (100%) rename src/{components/video => video/components/__old}/hooks/useVideoPlayer.ts (100%) rename src/{components/video => video/components/__old}/hooks/utils.ts (100%) rename src/{components/video => video/components/__old}/hooks/volumeStore.ts (100%) rename src/{components/video => video/components/__old}/parts/VideoErrorBoundary.tsx (100%) rename src/{components/video => video/components/__old}/parts/VideoPlayerError.tsx (100%) rename src/{components/video => video/components/__old}/parts/VideoPlayerHeader.tsx (100%) rename src/{components/video => video/components/__old}/parts/VideoPlayerIconButton.tsx (100%) rename src/{components/video => video/components/__old}/parts/VideoPopout.tsx (100%) create mode 100644 src/video/state/cache.ts create mode 100644 src/video/state/events.ts create mode 100644 src/video/state/hooks.tsx create mode 100644 src/video/state/init.ts create mode 100644 src/video/state/providers/providerTypes.ts create mode 100644 src/video/state/providers/videoStateProvider.ts create mode 100644 src/video/state/types.ts diff --git a/src/video/components/VideoPlayerBase.tsx b/src/video/components/VideoPlayerBase.tsx new file mode 100644 index 00000000..b151250b --- /dev/null +++ b/src/video/components/VideoPlayerBase.tsx @@ -0,0 +1,19 @@ +import { VideoPlayerContextProvider } from "../state/hooks"; + +export interface VideoPlayerProps { + children?: React.ReactNode; +} + +export function VideoPlayer(props: VideoPlayerProps) { + // TODO error boundary + // TODO move error boundary to only decorated, shouldn't have styling + // TODO internal controls + + return ( + +
+
{props.children}
+
+
+ ); +} diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/video/components/__old/DecoratedVideoPlayer.tsx similarity index 100% rename from src/components/video/DecoratedVideoPlayer.tsx rename to src/video/components/__old/DecoratedVideoPlayer.tsx diff --git a/src/components/video/VideoContext.tsx b/src/video/components/__old/VideoContext.tsx similarity index 100% rename from src/components/video/VideoContext.tsx rename to src/video/components/__old/VideoContext.tsx diff --git a/src/components/video/VideoPlayer.tsx b/src/video/components/__old/VideoPlayer.tsx similarity index 96% rename from src/components/video/VideoPlayer.tsx rename to src/video/components/__old/VideoPlayer.tsx index 3e064aa1..b16416d5 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/video/components/__old/VideoPlayer.tsx @@ -1,12 +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 { VideoErrorBoundary } from "../../components/video/parts/VideoErrorBoundary"; import { useVideoPlayerState, VideoPlayerContext, VideoPlayerContextProvider, -} from "./VideoContext"; +} from "../../video/components./../components/video/VideoContext"; export interface VideoPlayerProps { autoPlay?: boolean; diff --git a/src/components/video/controls/AirplayControl.tsx b/src/video/components/__old/controls/AirplayControl.tsx similarity index 100% rename from src/components/video/controls/AirplayControl.tsx rename to src/video/components/__old/controls/AirplayControl.tsx diff --git a/src/components/video/controls/BackdropControl.tsx b/src/video/components/__old/controls/BackdropControl.tsx similarity index 100% rename from src/components/video/controls/BackdropControl.tsx rename to src/video/components/__old/controls/BackdropControl.tsx diff --git a/src/components/video/controls/ChromeCastControl.tsx b/src/video/components/__old/controls/ChromeCastControl.tsx similarity index 100% rename from src/components/video/controls/ChromeCastControl.tsx rename to src/video/components/__old/controls/ChromeCastControl.tsx diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/video/components/__old/controls/FullscreenControl.tsx similarity index 100% rename from src/components/video/controls/FullscreenControl.tsx rename to src/video/components/__old/controls/FullscreenControl.tsx diff --git a/src/components/video/controls/LoadingControl.tsx b/src/video/components/__old/controls/LoadingControl.tsx similarity index 100% rename from src/components/video/controls/LoadingControl.tsx rename to src/video/components/__old/controls/LoadingControl.tsx diff --git a/src/components/video/controls/MiddlePauseControl.tsx b/src/video/components/__old/controls/MiddlePauseControl.tsx similarity index 100% rename from src/components/video/controls/MiddlePauseControl.tsx rename to src/video/components/__old/controls/MiddlePauseControl.tsx diff --git a/src/components/video/controls/MobileCenterControl.tsx b/src/video/components/__old/controls/MobileCenterControl.tsx similarity index 100% rename from src/components/video/controls/MobileCenterControl.tsx rename to src/video/components/__old/controls/MobileCenterControl.tsx diff --git a/src/components/video/controls/PageTitleControl.tsx b/src/video/components/__old/controls/PageTitleControl.tsx similarity index 100% rename from src/components/video/controls/PageTitleControl.tsx rename to src/video/components/__old/controls/PageTitleControl.tsx diff --git a/src/components/video/controls/PauseControl.tsx b/src/video/components/__old/controls/PauseControl.tsx similarity index 100% rename from src/components/video/controls/PauseControl.tsx rename to src/video/components/__old/controls/PauseControl.tsx diff --git a/src/components/video/controls/ProgressControl.tsx b/src/video/components/__old/controls/ProgressControl.tsx similarity index 100% rename from src/components/video/controls/ProgressControl.tsx rename to src/video/components/__old/controls/ProgressControl.tsx diff --git a/src/components/video/controls/ProgressListenerControl.tsx b/src/video/components/__old/controls/ProgressListenerControl.tsx similarity index 100% rename from src/components/video/controls/ProgressListenerControl.tsx rename to src/video/components/__old/controls/ProgressListenerControl.tsx diff --git a/src/components/video/controls/QualityDisplayControl.tsx b/src/video/components/__old/controls/QualityDisplayControl.tsx similarity index 100% rename from src/components/video/controls/QualityDisplayControl.tsx rename to src/video/components/__old/controls/QualityDisplayControl.tsx diff --git a/src/components/video/controls/SeriesSelectionControl.tsx b/src/video/components/__old/controls/SeriesSelectionControl.tsx similarity index 100% rename from src/components/video/controls/SeriesSelectionControl.tsx rename to src/video/components/__old/controls/SeriesSelectionControl.tsx diff --git a/src/components/video/controls/ShowControl.tsx b/src/video/components/__old/controls/ShowControl.tsx similarity index 100% rename from src/components/video/controls/ShowControl.tsx rename to src/video/components/__old/controls/ShowControl.tsx diff --git a/src/components/video/controls/ShowTitleControl.tsx b/src/video/components/__old/controls/ShowTitleControl.tsx similarity index 100% rename from src/components/video/controls/ShowTitleControl.tsx rename to src/video/components/__old/controls/ShowTitleControl.tsx diff --git a/src/components/video/controls/SkipTime.tsx b/src/video/components/__old/controls/SkipTime.tsx similarity index 100% rename from src/components/video/controls/SkipTime.tsx rename to src/video/components/__old/controls/SkipTime.tsx diff --git a/src/components/video/controls/SourceControl.tsx b/src/video/components/__old/controls/SourceControl.tsx similarity index 100% rename from src/components/video/controls/SourceControl.tsx rename to src/video/components/__old/controls/SourceControl.tsx diff --git a/src/components/video/controls/SourceSelectionControl.tsx b/src/video/components/__old/controls/SourceSelectionControl.tsx similarity index 100% rename from src/components/video/controls/SourceSelectionControl.tsx rename to src/video/components/__old/controls/SourceSelectionControl.tsx diff --git a/src/components/video/controls/TimeControl.tsx b/src/video/components/__old/controls/TimeControl.tsx similarity index 100% rename from src/components/video/controls/TimeControl.tsx rename to src/video/components/__old/controls/TimeControl.tsx diff --git a/src/components/video/controls/VolumeControl.tsx b/src/video/components/__old/controls/VolumeControl.tsx similarity index 100% rename from src/components/video/controls/VolumeControl.tsx rename to src/video/components/__old/controls/VolumeControl.tsx diff --git a/src/components/video/hooks/controlVideo.ts b/src/video/components/__old/hooks/controlVideo.ts similarity index 100% rename from src/components/video/hooks/controlVideo.ts rename to src/video/components/__old/hooks/controlVideo.ts diff --git a/src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/__old/hooks/useCurrentSeriesEpisodeInfo.ts similarity index 100% rename from src/components/video/hooks/useCurrentSeriesEpisodeInfo.ts rename to src/video/components/__old/hooks/useCurrentSeriesEpisodeInfo.ts diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/video/components/__old/hooks/useVideoPlayer.ts similarity index 100% rename from src/components/video/hooks/useVideoPlayer.ts rename to src/video/components/__old/hooks/useVideoPlayer.ts diff --git a/src/components/video/hooks/utils.ts b/src/video/components/__old/hooks/utils.ts similarity index 100% rename from src/components/video/hooks/utils.ts rename to src/video/components/__old/hooks/utils.ts diff --git a/src/components/video/hooks/volumeStore.ts b/src/video/components/__old/hooks/volumeStore.ts similarity index 100% rename from src/components/video/hooks/volumeStore.ts rename to src/video/components/__old/hooks/volumeStore.ts diff --git a/src/components/video/parts/VideoErrorBoundary.tsx b/src/video/components/__old/parts/VideoErrorBoundary.tsx similarity index 100% rename from src/components/video/parts/VideoErrorBoundary.tsx rename to src/video/components/__old/parts/VideoErrorBoundary.tsx diff --git a/src/components/video/parts/VideoPlayerError.tsx b/src/video/components/__old/parts/VideoPlayerError.tsx similarity index 100% rename from src/components/video/parts/VideoPlayerError.tsx rename to src/video/components/__old/parts/VideoPlayerError.tsx diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/video/components/__old/parts/VideoPlayerHeader.tsx similarity index 100% rename from src/components/video/parts/VideoPlayerHeader.tsx rename to src/video/components/__old/parts/VideoPlayerHeader.tsx diff --git a/src/components/video/parts/VideoPlayerIconButton.tsx b/src/video/components/__old/parts/VideoPlayerIconButton.tsx similarity index 100% rename from src/components/video/parts/VideoPlayerIconButton.tsx rename to src/video/components/__old/parts/VideoPlayerIconButton.tsx diff --git a/src/components/video/parts/VideoPopout.tsx b/src/video/components/__old/parts/VideoPopout.tsx similarity index 100% rename from src/components/video/parts/VideoPopout.tsx rename to src/video/components/__old/parts/VideoPopout.tsx diff --git a/src/video/state/cache.ts b/src/video/state/cache.ts new file mode 100644 index 00000000..4c5becb1 --- /dev/null +++ b/src/video/state/cache.ts @@ -0,0 +1,9 @@ +import { VideoPlayerState } from "./types"; + +export const _players: Map = new Map(); + +export function getPlayerState(descriptor: string): VideoPlayerState { + const state = _players.get(descriptor); + if (!state) throw new Error("invalid descriptor or has been unregistered"); + return state; +} diff --git a/src/video/state/events.ts b/src/video/state/events.ts new file mode 100644 index 00000000..08794991 --- /dev/null +++ b/src/video/state/events.ts @@ -0,0 +1,28 @@ +export type VideoPlayerEvent = "progress"; + +function createEventString(id: string, event: VideoPlayerEvent): string { + return `_vid:::${id}:::${event}`; +} + +export function sendEvent(id: string, event: VideoPlayerEvent, data: T) { + const evObj = new CustomEvent(createEventString(id, event), { + detail: data, + }); + document.dispatchEvent(evObj); +} + +export function listenEvent( + id: string, + event: VideoPlayerEvent, + cb: (data: T) => void +) { + document.addEventListener(createEventString(id, event), cb); +} + +export function unlistenEvent( + id: string, + event: VideoPlayerEvent, + cb: (data: T) => void +) { + document.removeEventListener(createEventString(id, event), cb); +} diff --git a/src/video/state/hooks.tsx b/src/video/state/hooks.tsx new file mode 100644 index 00000000..c320d81b --- /dev/null +++ b/src/video/state/hooks.tsx @@ -0,0 +1,36 @@ +import { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from "react"; +import { registerVideoPlayer, unregisterVideoPlayer } from "./init"; + +const VideoPlayerContext = createContext(""); + +export function VideoPlayerContextProvider(props: { children: ReactNode }) { + const [id, setId] = useState(null); + + useEffect(() => { + const vidId = registerVideoPlayer(); + setId(vidId); + + return () => { + unregisterVideoPlayer(vidId); + }; + }, [setId]); + + if (!id) return null; + + return ( + + {props.children} + + ); +} + +export function useVideoPlayerDescriptor(): string { + const id = useContext(VideoPlayerContext); + return id; +} diff --git a/src/video/state/init.ts b/src/video/state/init.ts new file mode 100644 index 00000000..afe672f9 --- /dev/null +++ b/src/video/state/init.ts @@ -0,0 +1,44 @@ +import { nanoid } from "nanoid"; +import { _players } from "./cache"; +import { VideoPlayerState } from "./types"; + +function initPlayer(): VideoPlayerState { + return { + isPlaying: false, + isPaused: true, + isFullscreen: false, + isFocused: false, + isLoading: false, + isSeeking: false, + isFirstLoading: true, + time: 0, + duration: 0, + volume: 0, + buffered: 0, + pausedWhenSeeking: false, + hasInitialized: false, + leftControlHovering: false, + hasPlayedOnce: false, + error: null, + popout: null, + seasonData: { + isSeries: false, + }, + canAirplay: false, + }; +} + +export function registerVideoPlayer(): string { + const id = nanoid(); + + if (_players.has(id)) { + throw new Error("duplicate id"); + } + + _players.set(id, initPlayer()); + return id; +} + +export function unregisterVideoPlayer(id: string) { + if (_players.has(id)) _players.delete(id); +} diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts new file mode 100644 index 00000000..8f5d4795 --- /dev/null +++ b/src/video/state/providers/providerTypes.ts @@ -0,0 +1,7 @@ +export type VideoPlayerStateProvider = { + pause: () => void; + play: () => void; + providerStart: () => { + destroy: () => void; + }; +}; diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts new file mode 100644 index 00000000..7104ad65 --- /dev/null +++ b/src/video/state/providers/videoStateProvider.ts @@ -0,0 +1,40 @@ +import { getPlayerState } from "../cache"; +import { VideoPlayerStateProvider } from "./providerTypes"; + +export function createVideoStateProvider( + descriptor: string, + player: HTMLVideoElement +): VideoPlayerStateProvider { + const state = getPlayerState(descriptor); + + return { + play() { + player.play(); + }, + pause() { + player.pause(); + }, + providerStart() { + // TODO reactivity through events + const pause = () => { + state.isPaused = true; + state.isPlaying = false; + }; + const playing = () => { + state.isPaused = false; + state.isPlaying = true; + state.isLoading = false; + state.hasPlayedOnce = true; + }; + + player.addEventListener("pause", pause); + player.addEventListener("playing", playing); + return { + destroy: () => { + player.removeEventListener("pause", pause); + player.removeEventListener("playing", playing); + }, + }; + }, + }; +} diff --git a/src/video/state/types.ts b/src/video/state/types.ts new file mode 100644 index 00000000..f7ec3893 --- /dev/null +++ b/src/video/state/types.ts @@ -0,0 +1,36 @@ +export type VideoPlayerState = { + isPlaying: boolean; + isPaused: boolean; + isSeeking: boolean; + isLoading: boolean; + isFirstLoading: boolean; + isFullscreen: boolean; + time: number; + duration: number; + volume: number; + buffered: number; + pausedWhenSeeking: boolean; + hasInitialized: boolean; + leftControlHovering: boolean; + hasPlayedOnce: boolean; + popout: string | null; + isFocused: boolean; + seasonData: { + isSeries: boolean; + current?: { + episodeId: string; + seasonId: string; + }; + seasons?: { + id: string; + number: number; + title: string; + episodes?: { id: string; number: number; title: string }[]; + }[]; + }; + error: null | { + name: string; + description: string; + }; + canAirplay: boolean; +}; diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index d9f36da9..dda6242e 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -1,7 +1,7 @@ import { useHistory, useParams } from "react-router-dom"; import { Helmet } from "react-helmet"; import { useEffect, useRef, useState } from "react"; -import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; +import { DecoratedVideoPlayer } from "@/video/components/__old/DecoratedVideoPlayer"; import { MWStream } from "@/backend/helpers/streams"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";