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",
|
||||
"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",
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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";
|
||||
|
|
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 "./Skips";
|
||||
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() {
|
||||
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();
|
||||
},
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export type DisplayInterfaceEvents = {
|
|||
time: number;
|
||||
duration: number;
|
||||
buffered: number;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||
|
|
|
@ -13,6 +13,9 @@ function useDisplayInterface() {
|
|||
if (!display) {
|
||||
setDisplay(makeVideoElementDisplayInterface());
|
||||
}
|
||||
return () => {
|
||||
if (display) setDisplay(null);
|
||||
};
|
||||
}, [display, setDisplay]);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
|||
<ScrapingPart
|
||||
media={{
|
||||
type: "movie",
|
||||
title:
|
||||
"Everything Everywhere All At Once bsbasjkdsakjdashjdasjhkds",
|
||||
title: "Everything Everywhere All At Once",
|
||||
tmdbId: "545611",
|
||||
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}
|
||||
|
||||
<Player.BlackOverlay show={desktopControlsVisible} />
|
||||
|
||||
<Player.CenterControls>
|
||||
<Player.LoadingSpinner />
|
||||
<AutoPlayStart />
|
||||
</Player.CenterControls>
|
||||
|
||||
<Player.TopControls show={desktopControlsVisible}>
|
||||
<div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center">
|
||||
<div className="flex space-x-3 items-center">
|
||||
|
@ -41,6 +60,7 @@ export function PlayerView() {
|
|||
</div>
|
||||
</div>
|
||||
</Player.TopControls>
|
||||
|
||||
<Player.BottomControls show={desktopControlsVisible}>
|
||||
<Player.ProgressBar />
|
||||
<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 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<ProviderControls["runAll"]>) => 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]);
|
||||
|
||||
|
|
|
@ -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<DisplaySlice> = (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<DisplaySlice> = (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<DisplaySlice> = (set, get) => ({
|
|||
s.progress.buffered = buffered;
|
||||
})
|
||||
);
|
||||
newDisplay.on("loading", (isLoading) =>
|
||||
set((s) => {
|
||||
s.mediaPlaying.isLoading = isLoading;
|
||||
})
|
||||
);
|
||||
|
||||
set((s) => {
|
||||
s.display = newDisplay;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue