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"