diff --git a/package.json b/package.json index 0c5fb382..fa2d0283 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "tailwind-scrollbar": "^2.0.1", "tailwindcss": "^3.2.4", "tailwindcss-themer": "^3.1.0", + "type-fest": "^4.3.3", "typescript": "^4.6.4", "vite": "^4.0.1", "vite-plugin-checker": "^0.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba43a93d..05eba9b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ devDependencies: tailwindcss-themer: specifier: ^3.1.0 version: 3.1.0(tailwindcss@3.3.3) + type-fest: + specifier: ^4.3.3 + version: 4.3.3 typescript: specifier: ^4.6.4 version: 4.9.5 @@ -6100,6 +6103,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} + engines: {node: '>=16'} + dev: true + /typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 74407269..c9aad803 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -1,6 +1,7 @@ export * from "./atoms"; export * from "./base/Container"; export * from "./base/TopControls"; +export * from "./base/CenterControls"; export * from "./base/BottomControls"; export * from "./base/BlackOverlay"; export * from "./base/BackLink"; diff --git a/src/components/player/atoms/AutoPlayStart.tsx b/src/components/player/atoms/AutoPlayStart.tsx new file mode 100644 index 00000000..a0019391 --- /dev/null +++ b/src/components/player/atoms/AutoPlayStart.tsx @@ -0,0 +1,34 @@ +import { useCallback } from "react"; + +import { Icon, Icons } from "@/components/Icon"; +import { playerStatus } from "@/stores/player/slices/source"; +import { usePlayerStore } from "@/stores/player/store"; + +export function AutoPlayStart() { + const display = usePlayerStore((s) => s.display); + const isPlaying = usePlayerStore((s) => s.mediaPlaying.isPlaying); + const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); + const hasPlayedOnce = usePlayerStore((s) => s.mediaPlaying.hasPlayedOnce); + const status = usePlayerStore((s) => s.status); + + const handleClick = useCallback(() => { + display?.play(); + }, [display]); + + if (hasPlayedOnce) return null; + if (isPlaying) return null; + if (isLoading) return null; + if (status !== playerStatus.PLAYING) return null; + + return ( +
+ +
+ ); +} diff --git a/src/components/player/atoms/LoadingSpinner.tsx b/src/components/player/atoms/LoadingSpinner.tsx new file mode 100644 index 00000000..11060b4f --- /dev/null +++ b/src/components/player/atoms/LoadingSpinner.tsx @@ -0,0 +1,10 @@ +import { Spinner } from "@/components/layout/Spinner"; +import { usePlayerStore } from "@/stores/player/store"; + +export function LoadingSpinner() { + const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); + + if (!isLoading) return null; + + return ; +} diff --git a/src/components/player/atoms/index.ts b/src/components/player/atoms/index.ts index f59c0183..0ac37749 100644 --- a/src/components/player/atoms/index.ts +++ b/src/components/player/atoms/index.ts @@ -3,3 +3,5 @@ export * from "./Fullscreen"; export * from "./ProgressBar"; export * from "./Skips"; export * from "./Time"; +export * from "./LoadingSpinner"; +export * from "./AutoPlayStart"; diff --git a/src/components/player/base/CenterControls.tsx b/src/components/player/base/CenterControls.tsx new file mode 100644 index 00000000..478b9234 --- /dev/null +++ b/src/components/player/base/CenterControls.tsx @@ -0,0 +1,7 @@ +export function CenterControls(props: { children: React.ReactNode }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index f92bdeee..ef3a5a88 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -26,8 +26,14 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { function setSource() { if (!videoElement || !source) return; videoElement.src = source.url; - videoElement.addEventListener("play", () => emit("play", undefined)); + videoElement.addEventListener("play", () => { + emit("play", undefined); + emit("loading", false); + }); + videoElement.addEventListener("playing", () => emit("play", undefined)); videoElement.addEventListener("pause", () => emit("pause", undefined)); + videoElement.addEventListener("canplay", () => emit("loading", false)); + videoElement.addEventListener("waiting", () => emit("loading", true)); videoElement.addEventListener("volumechange", () => emit("volumechange", videoElement?.volume ?? 0) ); @@ -57,10 +63,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { on, off, destroy: () => { + if (videoElement) { + videoElement.src = ""; + videoElement.remove(); + } fscreen.removeEventListener("fullscreenchange", fullscreenChange); }, load(newSource) { source = newSource; + emit("loading", true); setSource(); }, diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index 6d5e1417..6b037846 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -9,6 +9,7 @@ export type DisplayInterfaceEvents = { time: number; duration: number; buffered: number; + loading: boolean; }; export interface DisplayInterface extends Listener { diff --git a/src/components/player/internals/VideoContainer.tsx b/src/components/player/internals/VideoContainer.tsx index 9b6dbe2c..f4b372ac 100644 --- a/src/components/player/internals/VideoContainer.tsx +++ b/src/components/player/internals/VideoContainer.tsx @@ -13,6 +13,9 @@ function useDisplayInterface() { if (!display) { setDisplay(makeVideoElementDisplayInterface()); } + return () => { + if (display) setDisplay(null); + }; }, [display, setDisplay]); } diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 21e98e63..597f9d97 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -1,12 +1,14 @@ +import { MWStreamType } from "@/backend/helpers/streams"; import { BrandPill } from "@/components/layout/BrandPill"; import { Player } from "@/components/player"; +import { AutoPlayStart } from "@/components/player/atoms"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { playerStatus } from "@/stores/player/slices/source"; export function PlayerView() { - const { status, setScrapeStatus } = usePlayer(); + const { status, setScrapeStatus, playMedia } = usePlayer(); const desktopControlsVisible = useShouldShowControls(); return ( @@ -15,15 +17,32 @@ export function PlayerView() { { + if (out?.stream.type !== "file") return; + const qualities = Object.keys( + out.stream.qualities + ) as (keyof typeof out.stream.qualities)[]; + const file = out.stream.qualities[qualities[0]]; + if (!file) return; + playMedia({ + type: MWStreamType.MP4, + url: file.url, + }); + }} /> ) : null} + + + + + +
@@ -41,6 +60,7 @@ export function PlayerView() {
+
diff --git a/src/pages/parts/player/ScrapingPart.tsx b/src/pages/parts/player/ScrapingPart.tsx index 8e2cf700..585b2f3b 100644 --- a/src/pages/parts/player/ScrapingPart.tsx +++ b/src/pages/parts/player/ScrapingPart.tsx @@ -1,14 +1,14 @@ -import { ScrapeMedia } from "@movie-web/providers"; +import { ProviderControls, ScrapeMedia } from "@movie-web/providers"; import { useCallback, useEffect, useRef, useState } from "react"; +import type { AsyncReturnType } from "type-fest"; -import { MWStreamType } from "@/backend/helpers/streams"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { StatusCircle } from "@/components/player/internals/StatusCircle"; import { providers } from "@/utils/providers"; export interface ScrapingProps { media: ScrapeMedia; - // onGetStream?: () => void; + onGetStream?: (stream: AsyncReturnType) => void; } export interface ScrapingSegment { @@ -30,7 +30,7 @@ function useScrape() { const startScraping = useCallback( async (media: ScrapeMedia) => { - if (!providers) return; + if (!providers) return null; const output = await providers.runAll({ media, events: { @@ -118,12 +118,7 @@ export function ScrapingPart(props: ScrapingProps) { started.current = true; (async () => { const output = await startScraping(props.media); - if (output?.stream.type !== "file") return; - const firstFile = Object.values(output.stream.qualities)[0]; - playMedia({ - type: MWStreamType.MP4, - url: firstFile.url, - }); + props.onGetStream?.(output); })(); }, [startScraping, props, playMedia]); diff --git a/src/stores/player/slices/display.ts b/src/stores/player/slices/display.ts index c1bbefb9..bb2645bd 100644 --- a/src/stores/player/slices/display.ts +++ b/src/stores/player/slices/display.ts @@ -3,15 +3,22 @@ import { MakeSlice } from "@/stores/player/slices/types"; export interface DisplaySlice { display: DisplayInterface | null; - setDisplay(display: DisplayInterface): void; + setDisplay(display: DisplayInterface | null): void; } export const createDisplaySlice: MakeSlice = (set, get) => ({ display: null, - setDisplay(newDisplay: DisplayInterface) { + setDisplay(newDisplay: DisplayInterface | null) { const display = get().display; if (display) display.destroy(); + if (!newDisplay) { + set((s) => { + s.display = null; + }); + return; + } + // make display events update the state newDisplay.on("pause", () => set((s) => { @@ -21,6 +28,7 @@ export const createDisplaySlice: MakeSlice = (set, get) => ({ ); newDisplay.on("play", () => set((s) => { + s.mediaPlaying.hasPlayedOnce = true; s.mediaPlaying.isPaused = false; s.mediaPlaying.isPlaying = true; }) @@ -50,6 +58,11 @@ export const createDisplaySlice: MakeSlice = (set, get) => ({ s.progress.buffered = buffered; }) ); + newDisplay.on("loading", (isLoading) => + set((s) => { + s.mediaPlaying.isLoading = isLoading; + }) + ); set((s) => { s.display = newDisplay; diff --git a/src/stores/player/slices/playing.ts b/src/stores/player/slices/playing.ts index 28e6f5a6..26d6f2dd 100644 --- a/src/stores/player/slices/playing.ts +++ b/src/stores/player/slices/playing.ts @@ -7,7 +7,6 @@ export interface PlayingSlice { 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;