mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 04:31:51 +01:00
the start detaching video state from react
Co-authored-by: James Hawkins <jhawki2005@gmail.com>
This commit is contained in:
parent
73e6f26adb
commit
6ca3196b75
19
src/video/components/VideoPlayerBase.tsx
Normal file
19
src/video/components/VideoPlayerBase.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
9
src/video/state/cache.ts
Normal 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
28
src/video/state/events.ts
Normal 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
36
src/video/state/hooks.tsx
Normal 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
44
src/video/state/init.ts
Normal 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);
|
||||
}
|
7
src/video/state/providers/providerTypes.ts
Normal file
7
src/video/state/providers/providerTypes.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type VideoPlayerStateProvider = {
|
||||
pause: () => void;
|
||||
play: () => void;
|
||||
providerStart: () => {
|
||||
destroy: () => void;
|
||||
};
|
||||
};
|
40
src/video/state/providers/videoStateProvider.ts
Normal file
40
src/video/state/providers/videoStateProvider.ts
Normal 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
36
src/video/state/types.ts
Normal 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;
|
||||
};
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user