mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-21 14:47:41 +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
42 changed files with 222 additions and 3 deletions
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 { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
||||||
import { forwardRef, useContext, useEffect, useRef } from "react";
|
import { forwardRef, useContext, useEffect, useRef } from "react";
|
||||||
import { VideoErrorBoundary } from "./parts/VideoErrorBoundary";
|
import { VideoErrorBoundary } from "../../components/video/parts/VideoErrorBoundary";
|
||||||
import {
|
import {
|
||||||
useVideoPlayerState,
|
useVideoPlayerState,
|
||||||
VideoPlayerContext,
|
VideoPlayerContext,
|
||||||
VideoPlayerContextProvider,
|
VideoPlayerContextProvider,
|
||||||
} from "./VideoContext";
|
} from "../../video/components./../components/video/VideoContext";
|
||||||
|
|
||||||
export interface VideoPlayerProps {
|
export interface VideoPlayerProps {
|
||||||
autoPlay?: boolean;
|
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 { useHistory, useParams } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useEffect, useRef, useState } from "react";
|
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 { MWStream } from "@/backend/helpers/streams";
|
||||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||||
|
|
Loading…
Reference in a new issue