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
{props.children}
;
+ return (
+
+
+ {props.children}
+
+ );
}
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 ;
+ const player = usePlayerStore();
+ const videoEl = useRef(null);
+
+ useEffect(() => {
+ if (videoEl.current) videoEl.current.src = player.source?.url ?? "";
+ }, [player.source?.url]);
+
+ return ;
}
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 = () => ({
+ 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 = (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 = () => ({
+ 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;
+
+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 = (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 = 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((...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(obj: T | null): obj is T {
return obj != null;
}
+
+export type ValuesOf = 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 ;
}
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"