the start detaching video state from react

Co-authored-by: James Hawkins <jhawki2005@gmail.com>
This commit is contained in:
Jelle van Snik 2023-02-02 22:04:58 +01:00
parent 73e6f26adb
commit 6ca3196b75
42 changed files with 222 additions and 3 deletions

View File

@ -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, <VideoPlayer /> shouldn't have styling
// TODO internal controls
return (
<VideoPlayerContextProvider>
<div className="is-video-player relative h-full w-full select-none overflow-hidden bg-black [border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]">
<div className="absolute inset-0">{props.children}</div>
</div>
</VideoPlayerContextProvider>
);
}

View File

@ -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;

9
src/video/state/cache.ts Normal file
View File

@ -0,0 +1,9 @@
import { VideoPlayerState } from "./types";
export const _players: Map<string, VideoPlayerState> = 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;
}

28
src/video/state/events.ts Normal file
View File

@ -0,0 +1,28 @@
export type VideoPlayerEvent = "progress";
function createEventString(id: string, event: VideoPlayerEvent): string {
return `_vid:::${id}:::${event}`;
}
export function sendEvent<T>(id: string, event: VideoPlayerEvent, data: T) {
const evObj = new CustomEvent(createEventString(id, event), {
detail: data,
});
document.dispatchEvent(evObj);
}
export function listenEvent<T>(
id: string,
event: VideoPlayerEvent,
cb: (data: T) => void
) {
document.addEventListener<any>(createEventString(id, event), cb);
}
export function unlistenEvent<T>(
id: string,
event: VideoPlayerEvent,
cb: (data: T) => void
) {
document.removeEventListener<any>(createEventString(id, event), cb);
}

36
src/video/state/hooks.tsx Normal file
View File

@ -0,0 +1,36 @@
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { registerVideoPlayer, unregisterVideoPlayer } from "./init";
const VideoPlayerContext = createContext<string>("");
export function VideoPlayerContextProvider(props: { children: ReactNode }) {
const [id, setId] = useState<string | null>(null);
useEffect(() => {
const vidId = registerVideoPlayer();
setId(vidId);
return () => {
unregisterVideoPlayer(vidId);
};
}, [setId]);
if (!id) return null;
return (
<VideoPlayerContext.Provider value={id}>
{props.children}
</VideoPlayerContext.Provider>
);
}
export function useVideoPlayerDescriptor(): string {
const id = useContext(VideoPlayerContext);
return id;
}

44
src/video/state/init.ts Normal file
View File

@ -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);
}

View File

@ -0,0 +1,7 @@
export type VideoPlayerStateProvider = {
pause: () => void;
play: () => void;
providerStart: () => {
destroy: () => void;
};
};

View File

@ -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);
},
};
},
};
}

36
src/video/state/types.ts Normal file
View File

@ -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;
};

View File

@ -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";