pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Co-authored-by: William Oldham <github@binaryoverload.co.uk>

mrjvs 2d106ec7 dcb199a1

+122 -17
+1
package.json
··· 101 101 "tailwind-scrollbar": "^2.0.1", 102 102 "tailwindcss": "^3.2.4", 103 103 "tailwindcss-themer": "^3.1.0", 104 + "type-fest": "^4.3.3", 104 105 "typescript": "^4.6.4", 105 106 "vite": "^4.0.1", 106 107 "vite-plugin-checker": "^0.5.6",
+8
pnpm-lock.yaml
··· 229 229 tailwindcss-themer: 230 230 specifier: ^3.1.0 231 231 version: 3.1.0(tailwindcss@3.3.3) 232 + type-fest: 233 + specifier: ^4.3.3 234 + version: 4.3.3 232 235 typescript: 233 236 specifier: ^4.6.4 234 237 version: 4.9.5 ··· 6098 6101 /type-fest@0.21.3: 6099 6102 resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} 6100 6103 engines: {node: '>=10'} 6104 + dev: true 6105 + 6106 + /type-fest@4.3.3: 6107 + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} 6108 + engines: {node: '>=16'} 6101 6109 dev: true 6102 6110 6103 6111 /typed-array-buffer@1.0.0:
+1
src/components/player/Player.tsx
··· 1 1 export * from "./atoms"; 2 2 export * from "./base/Container"; 3 3 export * from "./base/TopControls"; 4 + export * from "./base/CenterControls"; 4 5 export * from "./base/BottomControls"; 5 6 export * from "./base/BlackOverlay"; 6 7 export * from "./base/BackLink";
+34
src/components/player/atoms/AutoPlayStart.tsx
··· 1 + import { useCallback } from "react"; 2 + 3 + import { Icon, Icons } from "@/components/Icon"; 4 + import { playerStatus } from "@/stores/player/slices/source"; 5 + import { usePlayerStore } from "@/stores/player/store"; 6 + 7 + export function AutoPlayStart() { 8 + const display = usePlayerStore((s) => s.display); 9 + const isPlaying = usePlayerStore((s) => s.mediaPlaying.isPlaying); 10 + const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); 11 + const hasPlayedOnce = usePlayerStore((s) => s.mediaPlaying.hasPlayedOnce); 12 + const status = usePlayerStore((s) => s.status); 13 + 14 + const handleClick = useCallback(() => { 15 + display?.play(); 16 + }, [display]); 17 + 18 + if (hasPlayedOnce) return null; 19 + if (isPlaying) return null; 20 + if (isLoading) return null; 21 + if (status !== playerStatus.PLAYING) return null; 22 + 23 + return ( 24 + <div 25 + onClick={handleClick} 26 + 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" 27 + > 28 + <Icon 29 + icon={Icons.PLAY} 30 + className="text-2xl transition-transform group-hover:scale-125" 31 + /> 32 + </div> 33 + ); 34 + }
+10
src/components/player/atoms/LoadingSpinner.tsx
··· 1 + import { Spinner } from "@/components/layout/Spinner"; 2 + import { usePlayerStore } from "@/stores/player/store"; 3 + 4 + export function LoadingSpinner() { 5 + const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); 6 + 7 + if (!isLoading) return null; 8 + 9 + return <Spinner />; 10 + }
+2
src/components/player/atoms/index.ts
··· 3 3 export * from "./ProgressBar"; 4 4 export * from "./Skips"; 5 5 export * from "./Time"; 6 + export * from "./LoadingSpinner"; 7 + export * from "./AutoPlayStart";
+7
src/components/player/base/CenterControls.tsx
··· 1 + export function CenterControls(props: { children: React.ReactNode }) { 2 + return ( 3 + <div className="absolute inset-0 flex items-center justify-center pointer-events-none [&>*]:pointer-events-auto"> 4 + {props.children} 5 + </div> 6 + ); 7 + }
+12 -1
src/components/player/display/base.ts
··· 26 26 function setSource() { 27 27 if (!videoElement || !source) return; 28 28 videoElement.src = source.url; 29 - videoElement.addEventListener("play", () => emit("play", undefined)); 29 + videoElement.addEventListener("play", () => { 30 + emit("play", undefined); 31 + emit("loading", false); 32 + }); 33 + videoElement.addEventListener("playing", () => emit("play", undefined)); 30 34 videoElement.addEventListener("pause", () => emit("pause", undefined)); 35 + videoElement.addEventListener("canplay", () => emit("loading", false)); 36 + videoElement.addEventListener("waiting", () => emit("loading", true)); 31 37 videoElement.addEventListener("volumechange", () => 32 38 emit("volumechange", videoElement?.volume ?? 0) 33 39 ); ··· 57 63 on, 58 64 off, 59 65 destroy: () => { 66 + if (videoElement) { 67 + videoElement.src = ""; 68 + videoElement.remove(); 69 + } 60 70 fscreen.removeEventListener("fullscreenchange", fullscreenChange); 61 71 }, 62 72 load(newSource) { 63 73 source = newSource; 74 + emit("loading", true); 64 75 setSource(); 65 76 }, 66 77
+1
src/components/player/display/displayInterface.ts
··· 9 9 time: number; 10 10 duration: number; 11 11 buffered: number; 12 + loading: boolean; 12 13 }; 13 14 14 15 export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
+3
src/components/player/internals/VideoContainer.tsx
··· 13 13 if (!display) { 14 14 setDisplay(makeVideoElementDisplayInterface()); 15 15 } 16 + return () => { 17 + if (display) setDisplay(null); 18 + }; 16 19 }, [display, setDisplay]); 17 20 } 18 21
+23 -3
src/pages/PlayerView.tsx
··· 1 + import { MWStreamType } from "@/backend/helpers/streams"; 1 2 import { BrandPill } from "@/components/layout/BrandPill"; 2 3 import { Player } from "@/components/player"; 4 + import { AutoPlayStart } from "@/components/player/atoms"; 3 5 import { usePlayer } from "@/components/player/hooks/usePlayer"; 4 6 import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; 5 7 import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; 6 8 import { playerStatus } from "@/stores/player/slices/source"; 7 9 8 10 export function PlayerView() { 9 - const { status, setScrapeStatus } = usePlayer(); 11 + const { status, setScrapeStatus, playMedia } = usePlayer(); 10 12 const desktopControlsVisible = useShouldShowControls(); 11 13 12 14 return ( ··· 15 17 <ScrapingPart 16 18 media={{ 17 19 type: "movie", 18 - title: 19 - "Everything Everywhere All At Once bsbasjkdsakjdashjdasjhkds", 20 + title: "Everything Everywhere All At Once", 20 21 tmdbId: "545611", 21 22 releaseYear: 2022, 22 23 }} 24 + onGetStream={(out) => { 25 + if (out?.stream.type !== "file") return; 26 + const qualities = Object.keys( 27 + out.stream.qualities 28 + ) as (keyof typeof out.stream.qualities)[]; 29 + const file = out.stream.qualities[qualities[0]]; 30 + if (!file) return; 31 + playMedia({ 32 + type: MWStreamType.MP4, 33 + url: file.url, 34 + }); 35 + }} 23 36 /> 24 37 ) : null} 25 38 26 39 <Player.BlackOverlay show={desktopControlsVisible} /> 40 + 41 + <Player.CenterControls> 42 + <Player.LoadingSpinner /> 43 + <AutoPlayStart /> 44 + </Player.CenterControls> 45 + 27 46 <Player.TopControls show={desktopControlsVisible}> 28 47 <div className="grid grid-cols-[1fr,auto] xl:grid-cols-3 items-center"> 29 48 <div className="flex space-x-3 items-center"> ··· 41 60 </div> 42 61 </div> 43 62 </Player.TopControls> 63 + 44 64 <Player.BottomControls show={desktopControlsVisible}> 45 65 <Player.ProgressBar /> 46 66 <div className="flex justify-between">
+5 -10
src/pages/parts/player/ScrapingPart.tsx
··· 1 - import { ScrapeMedia } from "@movie-web/providers"; 1 + import { ProviderControls, ScrapeMedia } from "@movie-web/providers"; 2 2 import { useCallback, useEffect, useRef, useState } from "react"; 3 + import type { AsyncReturnType } from "type-fest"; 3 4 4 - import { MWStreamType } from "@/backend/helpers/streams"; 5 5 import { usePlayer } from "@/components/player/hooks/usePlayer"; 6 6 import { StatusCircle } from "@/components/player/internals/StatusCircle"; 7 7 import { providers } from "@/utils/providers"; 8 8 9 9 export interface ScrapingProps { 10 10 media: ScrapeMedia; 11 - // onGetStream?: () => void; 11 + onGetStream?: (stream: AsyncReturnType<ProviderControls["runAll"]>) => void; 12 12 } 13 13 14 14 export interface ScrapingSegment { ··· 30 30 31 31 const startScraping = useCallback( 32 32 async (media: ScrapeMedia) => { 33 - if (!providers) return; 33 + if (!providers) return null; 34 34 const output = await providers.runAll({ 35 35 media, 36 36 events: { ··· 118 118 started.current = true; 119 119 (async () => { 120 120 const output = await startScraping(props.media); 121 - if (output?.stream.type !== "file") return; 122 - const firstFile = Object.values(output.stream.qualities)[0]; 123 - playMedia({ 124 - type: MWStreamType.MP4, 125 - url: firstFile.url, 126 - }); 121 + props.onGetStream?.(output); 127 122 })(); 128 123 }, [startScraping, props, playMedia]); 129 124
+15 -2
src/stores/player/slices/display.ts
··· 3 3 4 4 export interface DisplaySlice { 5 5 display: DisplayInterface | null; 6 - setDisplay(display: DisplayInterface): void; 6 + setDisplay(display: DisplayInterface | null): void; 7 7 } 8 8 9 9 export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({ 10 10 display: null, 11 - setDisplay(newDisplay: DisplayInterface) { 11 + setDisplay(newDisplay: DisplayInterface | null) { 12 12 const display = get().display; 13 13 if (display) display.destroy(); 14 14 15 + if (!newDisplay) { 16 + set((s) => { 17 + s.display = null; 18 + }); 19 + return; 20 + } 21 + 15 22 // make display events update the state 16 23 newDisplay.on("pause", () => 17 24 set((s) => { ··· 21 28 ); 22 29 newDisplay.on("play", () => 23 30 set((s) => { 31 + s.mediaPlaying.hasPlayedOnce = true; 24 32 s.mediaPlaying.isPaused = false; 25 33 s.mediaPlaying.isPlaying = true; 26 34 }) ··· 48 56 newDisplay.on("buffered", (buffered) => 49 57 set((s) => { 50 58 s.progress.buffered = buffered; 59 + }) 60 + ); 61 + newDisplay.on("loading", (isLoading) => 62 + set((s) => { 63 + s.mediaPlaying.isLoading = isLoading; 51 64 }) 52 65 ); 53 66
-1
src/stores/player/slices/playing.ts
··· 7 7 isSeeking: boolean; // seeking with progress bar 8 8 isDragSeeking: boolean; // is seeking for our custom progress bar 9 9 isLoading: boolean; // buffering or not 10 - isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing 11 10 hasPlayedOnce: boolean; // has the video played at all? 12 11 volume: number; 13 12 playbackSpeed: number;