basics of new video player state

This commit is contained in:
mrjvs 2023-07-23 16:30:22 +02:00
parent ba25c18390
commit 4bc8106cb3
17 changed files with 255 additions and 2 deletions

View File

@ -52,6 +52,7 @@ module.exports = {
"no-await-in-loop": "off", "no-await-in-loop": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
"prefer-destructuring": "off", "prefer-destructuring": "off",
"no-param-reassign": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"react/jsx-filename-extension": [ "react/jsx-filename-extension": [
"error", "error",

View File

@ -18,6 +18,7 @@
"hls.js": "^1.0.7", "hls.js": "^1.0.7",
"i18next": "^22.4.5", "i18next": "^22.4.5",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"immer": "^10.0.2",
"json5": "^2.2.0", "json5": "^2.2.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",

View File

@ -0,0 +1,3 @@
import { ReactNode } from "react";
export * as Atoms from "./atoms/index";
export

View File

@ -1,9 +1,16 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { VideoContainer } from "@/components/player/internals/VideoContainer";
export interface PlayerProps { export interface PlayerProps {
children?: ReactNode; children?: ReactNode;
} }
export function Container(props: PlayerProps) { export function Container(props: PlayerProps) {
return <div>{props.children}</div>; return (
<div>
<VideoContainer />
{props.children}
</div>
);
} }

View File

@ -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);
},
};
}

View File

@ -1,3 +1,14 @@
import { useEffect, useRef } from "react";
import { usePlayerStore } from "@/stores/player/store";
export function VideoContainer() { 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} />;
} }

View File

@ -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,
},
});

View File

@ -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;
});
},
});

View File

@ -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,
},
});

View File

@ -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,
};
});
},
});

View File

@ -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
>;

View File

@ -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),
}))
);

View File

@ -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 }[];
}[];
};

5
src/stores/video.ts Normal file
View File

@ -0,0 +1,5 @@
import { create } from "zustand";
export const useVideo = create(() => ({
}));

View File

@ -1,3 +1,5 @@
export function isNotNull<T>(obj: T | null): obj is T { export function isNotNull<T>(obj: T | null): obj is T {
return obj != null; return obj != null;
} }
export type ValuesOf<T> = T[keyof T];

View File

@ -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"; import { PlayerView } from "@/views/PlayerView";
export default function VideoTesterView() { 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 />; return <PlayerView />;
} }

View File

@ -3269,6 +3269,11 @@ immediate@~3.0.5:
resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== 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: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"