mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
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:
parent
dcb199a1fe
commit
2d106ec7ca
14 changed files with 122 additions and 17 deletions
|
@ -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",
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
34
src/components/player/atoms/AutoPlayStart.tsx
Normal file
34
src/components/player/atoms/AutoPlayStart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/player/atoms/LoadingSpinner.tsx
Normal file
10
src/components/player/atoms/LoadingSpinner.tsx
Normal 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 />;
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
7
src/components/player/base/CenterControls.tsx
Normal file
7
src/components/player/base/CenterControls.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -13,6 +13,9 @@ function useDisplayInterface() {
|
||||||
if (!display) {
|
if (!display) {
|
||||||
setDisplay(makeVideoElementDisplayInterface());
|
setDisplay(makeVideoElementDisplayInterface());
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
if (display) setDisplay(null);
|
||||||
|
};
|
||||||
}, [display, setDisplay]);
|
}, [display, setDisplay]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue