loading spinner, auto play start button + bug fix of multiple videos playing over each other

Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
mrjvs 2023-10-05 22:12:25 +02:00
parent dcb199a1fe
commit 2d106ec7ca
14 changed files with 122 additions and 17 deletions

View File

@ -101,6 +101,7 @@
"tailwind-scrollbar": "^2.0.1", "tailwind-scrollbar": "^2.0.1",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"tailwindcss-themer": "^3.1.0", "tailwindcss-themer": "^3.1.0",
"type-fest": "^4.3.3",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^4.0.1", "vite": "^4.0.1",
"vite-plugin-checker": "^0.5.6", "vite-plugin-checker": "^0.5.6",

8
pnpm-lock.yaml generated
View File

@ -229,6 +229,9 @@ devDependencies:
tailwindcss-themer: tailwindcss-themer:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(tailwindcss@3.3.3) version: 3.1.0(tailwindcss@3.3.3)
type-fest:
specifier: ^4.3.3
version: 4.3.3
typescript: typescript:
specifier: ^4.6.4 specifier: ^4.6.4
version: 4.9.5 version: 4.9.5
@ -6100,6 +6103,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/type-fest@4.3.3:
resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==}
engines: {node: '>=16'}
dev: true
/typed-array-buffer@1.0.0: /typed-array-buffer@1.0.0:
resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}

View File

@ -1,6 +1,7 @@
export * from "./atoms"; export * from "./atoms";
export * from "./base/Container"; export * from "./base/Container";
export * from "./base/TopControls"; export * from "./base/TopControls";
export * from "./base/CenterControls";
export * from "./base/BottomControls"; export * from "./base/BottomControls";
export * from "./base/BlackOverlay"; export * from "./base/BlackOverlay";
export * from "./base/BackLink"; export * from "./base/BackLink";

View File

@ -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 (
<div
onClick={handleClick}
className="group pointer-events-auto flex h-16 w-16 cursor-pointer items-center justify-center rounded-full bg-denim-400 text-white transition-[background-color,transform] hover:scale-125 hover:bg-denim-500 active:scale-100"
>
<Icon
icon={Icons.PLAY}
className="text-2xl transition-transform group-hover:scale-125"
/>
</div>
);
}

View File

@ -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 <Spinner />;
}

View File

@ -3,3 +3,5 @@ export * from "./Fullscreen";
export * from "./ProgressBar"; export * from "./ProgressBar";
export * from "./Skips"; export * from "./Skips";
export * from "./Time"; export * from "./Time";
export * from "./LoadingSpinner";
export * from "./AutoPlayStart";

View File

@ -0,0 +1,7 @@
export function CenterControls(props: { children: React.ReactNode }) {
return (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none [&>*]:pointer-events-auto">
{props.children}
</div>
);
}

View File

@ -26,8 +26,14 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
function setSource() { function setSource() {
if (!videoElement || !source) return; if (!videoElement || !source) return;
videoElement.src = source.url; 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("pause", () => emit("pause", undefined));
videoElement.addEventListener("canplay", () => emit("loading", false));
videoElement.addEventListener("waiting", () => emit("loading", true));
videoElement.addEventListener("volumechange", () => videoElement.addEventListener("volumechange", () =>
emit("volumechange", videoElement?.volume ?? 0) emit("volumechange", videoElement?.volume ?? 0)
); );
@ -57,10 +63,15 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
on, on,
off, off,
destroy: () => { destroy: () => {
if (videoElement) {
videoElement.src = "";
videoElement.remove();
}
fscreen.removeEventListener("fullscreenchange", fullscreenChange); fscreen.removeEventListener("fullscreenchange", fullscreenChange);
}, },
load(newSource) { load(newSource) {
source = newSource; source = newSource;
emit("loading", true);
setSource(); setSource();
}, },

View File

@ -9,6 +9,7 @@ export type DisplayInterfaceEvents = {
time: number; time: number;
duration: number; duration: number;
buffered: number; buffered: number;
loading: boolean;
}; };
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> { export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {

View File

@ -13,6 +13,9 @@ function useDisplayInterface() {
if (!display) { if (!display) {
setDisplay(makeVideoElementDisplayInterface()); setDisplay(makeVideoElementDisplayInterface());
} }
return () => {
if (display) setDisplay(null);
};
}, [display, setDisplay]); }, [display, setDisplay]);
} }

View File

@ -1,12 +1,14 @@
import { MWStreamType } from "@/backend/helpers/streams";
import { BrandPill } from "@/components/layout/BrandPill"; import { BrandPill } from "@/components/layout/BrandPill";
import { Player } from "@/components/player"; import { Player } from "@/components/player";
import { AutoPlayStart } from "@/components/player/atoms";
import { usePlayer } from "@/components/player/hooks/usePlayer"; import { usePlayer } from "@/components/player/hooks/usePlayer";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { playerStatus } from "@/stores/player/slices/source"; import { playerStatus } from "@/stores/player/slices/source";
export function PlayerView() { export function PlayerView() {
const { status, setScrapeStatus } = usePlayer(); const { status, setScrapeStatus, playMedia } = usePlayer();
const desktopControlsVisible = useShouldShowControls(); const desktopControlsVisible = useShouldShowControls();
return ( return (
@ -15,15 +17,32 @@ export function PlayerView() {
<ScrapingPart <ScrapingPart
media={{ media={{
type: "movie", type: "movie",
title: title: "Everything Everywhere All At Once",
"Everything Everywhere All At Once bsbasjkdsakjdashjdasjhkds",
tmdbId: "545611", tmdbId: "545611",
releaseYear: 2022, releaseYear: 2022,
}} }}
onGetStream={(out) => {
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} ) : null}
<Player.BlackOverlay show={desktopControlsVisible} /> <Player.BlackOverlay show={desktopControlsVisible} />
<Player.CenterControls>
<Player.LoadingSpinner />
<AutoPlayStart />
</Player.CenterControls>
<Player.TopControls show={desktopControlsVisible}> <Player.TopControls show={desktopControlsVisible}>
<div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center"> <div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center">
<div className="flex space-x-3 items-center"> <div className="flex space-x-3 items-center">
@ -41,6 +60,7 @@ export function PlayerView() {
</div> </div>
</div> </div>
</Player.TopControls> </Player.TopControls>
<Player.BottomControls show={desktopControlsVisible}> <Player.BottomControls show={desktopControlsVisible}>
<Player.ProgressBar /> <Player.ProgressBar />
<div className="flex justify-between"> <div className="flex justify-between">

View File

@ -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 { 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 { usePlayer } from "@/components/player/hooks/usePlayer";
import { StatusCircle } from "@/components/player/internals/StatusCircle"; import { StatusCircle } from "@/components/player/internals/StatusCircle";
import { providers } from "@/utils/providers"; import { providers } from "@/utils/providers";
export interface ScrapingProps { export interface ScrapingProps {
media: ScrapeMedia; media: ScrapeMedia;
// onGetStream?: () => void; onGetStream?: (stream: AsyncReturnType<ProviderControls["runAll"]>) => void;
} }
export interface ScrapingSegment { export interface ScrapingSegment {
@ -30,7 +30,7 @@ function useScrape() {
const startScraping = useCallback( const startScraping = useCallback(
async (media: ScrapeMedia) => { async (media: ScrapeMedia) => {
if (!providers) return; if (!providers) return null;
const output = await providers.runAll({ const output = await providers.runAll({
media, media,
events: { events: {
@ -118,12 +118,7 @@ export function ScrapingPart(props: ScrapingProps) {
started.current = true; started.current = true;
(async () => { (async () => {
const output = await startScraping(props.media); const output = await startScraping(props.media);
if (output?.stream.type !== "file") return; props.onGetStream?.(output);
const firstFile = Object.values(output.stream.qualities)[0];
playMedia({
type: MWStreamType.MP4,
url: firstFile.url,
});
})(); })();
}, [startScraping, props, playMedia]); }, [startScraping, props, playMedia]);

View File

@ -3,15 +3,22 @@ import { MakeSlice } from "@/stores/player/slices/types";
export interface DisplaySlice { export interface DisplaySlice {
display: DisplayInterface | null; display: DisplayInterface | null;
setDisplay(display: DisplayInterface): void; setDisplay(display: DisplayInterface | null): void;
} }
export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
display: null, display: null,
setDisplay(newDisplay: DisplayInterface) { setDisplay(newDisplay: DisplayInterface | null) {
const display = get().display; const display = get().display;
if (display) display.destroy(); if (display) display.destroy();
if (!newDisplay) {
set((s) => {
s.display = null;
});
return;
}
// make display events update the state // make display events update the state
newDisplay.on("pause", () => newDisplay.on("pause", () =>
set((s) => { set((s) => {
@ -21,6 +28,7 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
); );
newDisplay.on("play", () => newDisplay.on("play", () =>
set((s) => { set((s) => {
s.mediaPlaying.hasPlayedOnce = true;
s.mediaPlaying.isPaused = false; s.mediaPlaying.isPaused = false;
s.mediaPlaying.isPlaying = true; s.mediaPlaying.isPlaying = true;
}) })
@ -50,6 +58,11 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
s.progress.buffered = buffered; s.progress.buffered = buffered;
}) })
); );
newDisplay.on("loading", (isLoading) =>
set((s) => {
s.mediaPlaying.isLoading = isLoading;
})
);
set((s) => { set((s) => {
s.display = newDisplay; s.display = newDisplay;

View File

@ -7,7 +7,6 @@ export interface PlayingSlice {
isSeeking: boolean; // seeking with progress bar isSeeking: boolean; // seeking with progress bar
isDragSeeking: boolean; // is seeking for our custom progress bar isDragSeeking: boolean; // is seeking for our custom progress bar
isLoading: boolean; // buffering or not 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? hasPlayedOnce: boolean; // has the video played at all?
volume: number; volume: number;
playbackSpeed: number; playbackSpeed: number;