From 4bc8106cb37c4f5e34cd6ea25a3f72bde5857988 Mon Sep 17 00:00:00 2001 From: mrjvs <jellevs@gmail.com> Date: Sun, 23 Jul 2023 16:30:22 +0200 Subject: [PATCH] basics of new video player state --- .eslintrc.js | 1 + package.json | 1 + src/components/player/Player.tsx | 3 ++ src/components/player/base/Container.tsx | 9 +++- src/components/player/hooks/usePlayer.ts | 20 +++++++++ .../player/internals/VideoContainer.tsx | 13 +++++- src/stores/player/slices/interface.ts | 28 ++++++++++++ src/stores/player/slices/playing.ts | 43 +++++++++++++++++++ src/stores/player/slices/progress.ts | 19 ++++++++ src/stores/player/slices/source.ts | 39 +++++++++++++++++ src/stores/player/slices/types.ts | 17 ++++++++ src/stores/player/store.ts | 17 ++++++++ src/stores/player/types.ts | 22 ++++++++++ src/stores/video.ts | 5 +++ src/utils/typeguard.ts | 2 + src/views/developer/VideoTesterView.tsx | 13 ++++++ yarn.lock | 5 +++ 17 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/components/player/Player.tsx create mode 100644 src/components/player/hooks/usePlayer.ts create mode 100644 src/stores/player/slices/interface.ts create mode 100644 src/stores/player/slices/playing.ts create mode 100644 src/stores/player/slices/progress.ts create mode 100644 src/stores/player/slices/source.ts create mode 100644 src/stores/player/slices/types.ts create mode 100644 src/stores/player/store.ts create mode 100644 src/stores/player/types.ts create mode 100644 src/stores/video.ts diff --git a/.eslintrc.js b/.eslintrc.js index c1a2b2a7..a2da2b2a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,7 @@ module.exports = { "no-await-in-loop": "off", "no-nested-ternary": "off", "prefer-destructuring": "off", + "no-param-reassign": "off", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "react/jsx-filename-extension": [ "error", diff --git a/package.json b/package.json index 22fa2e07..ad03942a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "hls.js": "^1.0.7", "i18next": "^22.4.5", "i18next-browser-languagedetector": "^7.0.1", + "immer": "^10.0.2", "json5": "^2.2.0", "lodash.throttle": "^4.1.1", "nanoid": "^4.0.0", diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx new file mode 100644 index 00000000..4cd0f48e --- /dev/null +++ b/src/components/player/Player.tsx @@ -0,0 +1,3 @@ +import { ReactNode } from "react"; +export * as Atoms from "./atoms/index"; +export \ No newline at end of file diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 5b682393..d8e3cb93 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,9 +1,16 @@ import { ReactNode } from "react"; +import { VideoContainer } from "@/components/player/internals/VideoContainer"; + export interface PlayerProps { children?: ReactNode; } export function Container(props: PlayerProps) { - return <div>{props.children}</div>; + return ( + <div> + <VideoContainer /> + {props.children} + </div> + ); } diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts new file mode 100644 index 00000000..0db61398 --- /dev/null +++ b/src/components/player/hooks/usePlayer.ts @@ -0,0 +1,20 @@ +import { MWStreamType } from "@/backend/helpers/streams"; +import { playerStatus } from "@/stores/player/slices/source"; +import { usePlayerStore } from "@/stores/player/store"; + +export interface Source { + url: string; + type: MWStreamType; +} + +export function usePlayer() { + const setStatus = usePlayerStore((s) => s.setStatus); + const setSource = usePlayerStore((s) => s.setSource); + + return { + playMedia(source: Source) { + setSource(source.url, source.type); + setStatus(playerStatus.PLAYING); + }, + }; +} diff --git a/src/components/player/internals/VideoContainer.tsx b/src/components/player/internals/VideoContainer.tsx index 53907341..1cd28f51 100644 --- a/src/components/player/internals/VideoContainer.tsx +++ b/src/components/player/internals/VideoContainer.tsx @@ -1,3 +1,14 @@ +import { useEffect, useRef } from "react"; + +import { usePlayerStore } from "@/stores/player/store"; + export function VideoContainer() { - return <div />; + const player = usePlayerStore(); + const videoEl = useRef<HTMLVideoElement>(null); + + useEffect(() => { + if (videoEl.current) videoEl.current.src = player.source?.url ?? ""; + }, [player.source?.url]); + + return <video controls ref={videoEl} />; } diff --git a/src/stores/player/slices/interface.ts b/src/stores/player/slices/interface.ts new file mode 100644 index 00000000..53b1171e --- /dev/null +++ b/src/stores/player/slices/interface.ts @@ -0,0 +1,28 @@ +import { MakeSlice } from "@/stores/player/slices/types"; + +export enum VideoPlayerTimeFormat { + REGULAR = 0, + REMAINING = 1, +} + +export interface InterfaceSlice { + interface: { + isFullscreen: boolean; + + volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently? + volumeChangedWithKeybindDebounce: NodeJS.Timeout | null; // debounce for the duration of the "volume changed thingamajig" + + leftControlHovering: boolean; // is the cursor hovered over the left side of player controls + timeFormat: VideoPlayerTimeFormat; // Time format of the video player + }; +} + +export const createInterfaceSlice: MakeSlice<InterfaceSlice> = () => ({ + interface: { + isFullscreen: false, + leftControlHovering: false, + volumeChangedWithKeybind: false, + volumeChangedWithKeybindDebounce: null, + timeFormat: VideoPlayerTimeFormat.REGULAR, + }, +}); diff --git a/src/stores/player/slices/playing.ts b/src/stores/player/slices/playing.ts new file mode 100644 index 00000000..28e6f5a6 --- /dev/null +++ b/src/stores/player/slices/playing.ts @@ -0,0 +1,43 @@ +import { MakeSlice } from "@/stores/player/slices/types"; + +export interface PlayingSlice { + mediaPlaying: { + isPlaying: boolean; + isPaused: boolean; + isSeeking: boolean; // seeking with progress bar + isDragSeeking: boolean; // is seeking for our custom progress bar + isLoading: boolean; // buffering or not + isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing + hasPlayedOnce: boolean; // has the video played at all? + volume: number; + playbackSpeed: number; + }; + play(): void; + pause(): void; +} + +export const createPlayingSlice: MakeSlice<PlayingSlice> = (set) => ({ + mediaPlaying: { + isPlaying: false, + isPaused: true, + isLoading: false, + isSeeking: false, + isDragSeeking: false, + isFirstLoading: true, + hasPlayedOnce: false, + volume: 0, + playbackSpeed: 1, + }, + play() { + set((state) => { + state.mediaPlaying.isPlaying = true; + state.mediaPlaying.isPaused = false; + }); + }, + pause() { + set((state) => { + state.mediaPlaying.isPlaying = false; + state.mediaPlaying.isPaused = false; + }); + }, +}); diff --git a/src/stores/player/slices/progress.ts b/src/stores/player/slices/progress.ts new file mode 100644 index 00000000..4be4fc1d --- /dev/null +++ b/src/stores/player/slices/progress.ts @@ -0,0 +1,19 @@ +import { MakeSlice } from "@/stores/player/slices/types"; + +export interface ProgressSlice { + progress: { + time: number; // current time of video + duration: number; // length of video + buffered: number; // how much is buffered + draggingTime: number; // when dragging, time thats at the cursor + }; +} + +export const createProgressSlice: MakeSlice<ProgressSlice> = () => ({ + progress: { + time: 0, + duration: 0, + buffered: 0, + draggingTime: 0, + }, +}); diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts new file mode 100644 index 00000000..7504d83b --- /dev/null +++ b/src/stores/player/slices/source.ts @@ -0,0 +1,39 @@ +import { MWStreamType } from "@/backend/helpers/streams"; +import { MakeSlice } from "@/stores/player/slices/types"; +import { ValuesOf } from "@/utils/typeguard"; + +export const playerStatus = { + IDLE: "idle", + SCRAPING: "scraping", + PLAYING: "playing", +} as const; + +export type PlayerStatus = ValuesOf<typeof playerStatus>; + +export interface SourceSlice { + status: PlayerStatus; + source: { + url: string; + type: MWStreamType; + } | null; + setStatus(status: PlayerStatus): void; + setSource(url: string, type: MWStreamType): void; +} + +export const createSourceSlice: MakeSlice<SourceSlice> = (set) => ({ + source: null, + status: playerStatus.IDLE, + setStatus(status: PlayerStatus) { + set((s) => { + s.status = status; + }); + }, + setSource(url: string, type: MWStreamType) { + set((s) => { + s.source = { + type, + url, + }; + }); + }, +}); diff --git a/src/stores/player/slices/types.ts b/src/stores/player/slices/types.ts new file mode 100644 index 00000000..f2c5269a --- /dev/null +++ b/src/stores/player/slices/types.ts @@ -0,0 +1,17 @@ +import { StateCreator } from "zustand"; + +import { InterfaceSlice } from "@/stores/player/slices/interface"; +import { PlayingSlice } from "@/stores/player/slices/playing"; +import { ProgressSlice } from "@/stores/player/slices/progress"; +import { SourceSlice } from "@/stores/player/slices/source"; + +export type AllSlices = InterfaceSlice & + PlayingSlice & + ProgressSlice & + SourceSlice; +export type MakeSlice<Slice> = StateCreator< + AllSlices, + [["zustand/immer", never]], + [], + Slice +>; diff --git a/src/stores/player/store.ts b/src/stores/player/store.ts new file mode 100644 index 00000000..41216eec --- /dev/null +++ b/src/stores/player/store.ts @@ -0,0 +1,17 @@ +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +import { createInterfaceSlice } from "@/stores/player/slices/interface"; +import { createPlayingSlice } from "@/stores/player/slices/playing"; +import { createProgressSlice } from "@/stores/player/slices/progress"; +import { createSourceSlice } from "@/stores/player/slices/source"; +import { AllSlices } from "@/stores/player/slices/types"; + +export const usePlayerStore = create( + immer<AllSlices>((...a) => ({ + ...createInterfaceSlice(...a), + ...createProgressSlice(...a), + ...createPlayingSlice(...a), + ...createSourceSlice(...a), + })) +); diff --git a/src/stores/player/types.ts b/src/stores/player/types.ts new file mode 100644 index 00000000..b1a183dc --- /dev/null +++ b/src/stores/player/types.ts @@ -0,0 +1,22 @@ +import { MWCaption } from "@/backend/helpers/streams"; +import { DetailedMeta } from "@/backend/metadata/getmeta"; + +export interface Thumbnail { + from: number; + to: number; + imgUrl: string; +} +export type VideoPlayerMeta = { + meta: DetailedMeta; + captions: MWCaption[]; + episode?: { + episodeId: string; + seasonId: string; + }; + seasons?: { + id: string; + number: number; + title: string; + episodes?: { id: string; number: number; title: string }[]; + }[]; +}; diff --git a/src/stores/video.ts b/src/stores/video.ts new file mode 100644 index 00000000..9802896e --- /dev/null +++ b/src/stores/video.ts @@ -0,0 +1,5 @@ +import { create } from "zustand"; + +export const useVideo = create(() => ({ + +})); diff --git a/src/utils/typeguard.ts b/src/utils/typeguard.ts index 95dd81a1..f747b247 100644 --- a/src/utils/typeguard.ts +++ b/src/utils/typeguard.ts @@ -1,3 +1,5 @@ export function isNotNull<T>(obj: T | null): obj is T { return obj != null; } + +export type ValuesOf<T> = T[keyof T]; diff --git a/src/views/developer/VideoTesterView.tsx b/src/views/developer/VideoTesterView.tsx index c0c9cfc4..c07ddc32 100644 --- a/src/views/developer/VideoTesterView.tsx +++ b/src/views/developer/VideoTesterView.tsx @@ -1,5 +1,18 @@ +import { useEffect } from "react"; + +import { MWStreamType } from "@/backend/helpers/streams"; +import { usePlayer } from "@/components/player/hooks/usePlayer"; import { PlayerView } from "@/views/PlayerView"; export default function VideoTesterView() { + const player = usePlayer(); + + useEffect(() => { + player.playMedia({ + type: MWStreamType.MP4, + url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + }); + }); + return <PlayerView />; } diff --git a/yarn.lock b/yarn.lock index 6f000617..c4e29e2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3269,6 +3269,11 @@ immediate@~3.0.5: resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== +immer@^10.0.2, immer@>=9.0: + version "10.0.2" + resolved "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz" + integrity sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"