fix recursive rendering + show meta in player

This commit is contained in:
Jelle van Snik 2023-01-21 23:45:26 +01:00
parent b6a23aa0b7
commit 5a01a68ce4
11 changed files with 147 additions and 45 deletions

View File

@ -7,6 +7,7 @@ import { LoadingControl } from "./controls/LoadingControl";
import { MiddlePauseControl } from "./controls/MiddlePauseControl"; import { MiddlePauseControl } from "./controls/MiddlePauseControl";
import { PauseControl } from "./controls/PauseControl"; import { PauseControl } from "./controls/PauseControl";
import { ProgressControl } from "./controls/ProgressControl"; import { ProgressControl } from "./controls/ProgressControl";
import { ShowTitleControl } from "./controls/ShowTitleControl";
import { TimeControl } from "./controls/TimeControl"; import { TimeControl } from "./controls/TimeControl";
import { VolumeControl } from "./controls/VolumeControl"; import { VolumeControl } from "./controls/VolumeControl";
import { VideoPlayerError } from "./parts/VideoPlayerError"; import { VideoPlayerError } from "./parts/VideoPlayerError";
@ -30,15 +31,18 @@ function LeftSideControls() {
}, [videoState]); }, [videoState]);
return ( return (
<div <>
className="flex items-center px-2" <div
onMouseLeave={handleMouseLeave} className="flex items-center px-2"
onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
> onMouseEnter={handleMouseEnter}
<PauseControl /> >
<VolumeControl className="mr-2" /> <PauseControl />
<TimeControl /> <VolumeControl className="mr-2" />
</div> <TimeControl />
</div>
<ShowTitleControl />
</>
); );
} }

View File

@ -12,15 +12,14 @@ export function BackdropControl(props: BackdropControlProps) {
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null); const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const clickareaRef = useRef<HTMLDivElement>(null); const clickareaRef = useRef<HTMLDivElement>(null);
// TODO fix infinite loop
const handleMouseMove = useCallback(() => { const handleMouseMove = useCallback(() => {
setMoved(true); if (!moved) setMoved(true);
if (timeout.current) clearTimeout(timeout.current); if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => { timeout.current = setTimeout(() => {
setMoved(false); if (moved) setMoved(false);
timeout.current = null; timeout.current = null;
}, 3000); }, 3000);
}, [timeout, setMoved]); }, [setMoved, moved]);
const handleMouseLeave = useCallback(() => { const handleMouseLeave = useCallback(() => {
setMoved(false); setMoved(false);
@ -45,8 +44,13 @@ export function BackdropControl(props: BackdropControlProps) {
[videoState, clickareaRef] [videoState, clickareaRef]
); );
const lastBackdropValue = useRef<boolean | null>(null);
useEffect(() => { useEffect(() => {
props.onBackdropChange?.(moved || videoState.isPaused); const currentValue = moved || videoState.isPaused;
if (currentValue !== lastBackdropValue.current) {
lastBackdropValue.current = currentValue;
props.onBackdropChange?.(currentValue);
}
}, [videoState, moved, props]); }, [videoState, moved, props]);
const showUI = moved || videoState.isPaused; const showUI = moved || videoState.isPaused;

View File

@ -7,7 +7,6 @@ interface Props {
onProgress?: (time: number, duration: number) => void; onProgress?: (time: number, duration: number) => void;
} }
// TODO fix infinite loops
export function ProgressListenerControl(props: Props) { export function ProgressListenerControl(props: Props) {
const { videoState } = useVideoPlayerState(); const { videoState } = useVideoPlayerState();
const didInitialize = useRef<true | null>(null); const didInitialize = useRef<true | null>(null);

View File

@ -0,0 +1,26 @@
import { useEffect } from "react";
import { useVideoPlayerState } from "../VideoContext";
interface ShowControlProps {
series?: {
episode: number;
season: number;
};
title?: string;
}
export function ShowControl(props: ShowControlProps) {
const { videoState } = useVideoPlayerState();
useEffect(() => {
videoState.setShowData({
current: props.series,
isSeries: !!props.series,
title: props.title,
});
// we only want it to run when props change, not when videoState changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props]);
return null;
}

View File

@ -0,0 +1,19 @@
import { useVideoPlayerState } from "../VideoContext";
export function ShowTitleControl() {
const { videoState } = useVideoPlayerState();
if (!videoState.seasonData.isSeries) return null;
if (!videoState.seasonData.title || !videoState.seasonData.current)
return null;
const cur = videoState.seasonData.current;
const selectedText = `S${cur.season} E${cur.episode}`;
return (
<p className="ml-8 select-none space-x-2 font-bold text-white">
<span>{selectedText}</span>
<span className="opacity-50">{videoState.seasonData.title}</span>
</p>
);
}

View File

@ -11,6 +11,15 @@ import React, { RefObject } from "react";
import { PlayerState } from "./useVideoPlayer"; import { PlayerState } from "./useVideoPlayer";
import { getStoredVolume, setStoredVolume } from "./volumeStore"; import { getStoredVolume, setStoredVolume } from "./volumeStore";
interface ShowData {
current?: {
episode: number;
season: number;
};
isSeries: boolean;
title?: string;
}
export interface PlayerControls { export interface PlayerControls {
play(): void; play(): void;
pause(): void; pause(): void;
@ -21,6 +30,7 @@ export interface PlayerControls {
setSeeking(active: boolean): void; setSeeking(active: boolean): void;
setLeftControlsHover(hovering: boolean): void; setLeftControlsHover(hovering: boolean): void;
initPlayer(sourceUrl: string, sourceType: MWStreamType): void; initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
setShowData(data: ShowData): void;
} }
export const initialControls: PlayerControls = { export const initialControls: PlayerControls = {
@ -33,6 +43,7 @@ export const initialControls: PlayerControls = {
setSeeking: () => null, setSeeking: () => null,
setLeftControlsHover: () => null, setLeftControlsHover: () => null,
initPlayer: () => null, initPlayer: () => null,
setShowData: () => null,
}; };
export function populateControls( export function populateControls(
@ -105,6 +116,9 @@ export function populateControls(
setLeftControlsHover(hovering) { setLeftControlsHover(hovering) {
update((s) => ({ ...s, leftControlHovering: hovering })); update((s) => ({ ...s, leftControlHovering: hovering }));
}, },
setShowData(data) {
update((s) => ({ ...s, seasonData: data }));
},
initPlayer(sourceUrl: string, sourceType: MWStreamType) { initPlayer(sourceUrl: string, sourceType: MWStreamType) {
this.setVolume(getStoredVolume()); this.setVolume(getStoredVolume());

View File

@ -23,6 +23,14 @@ export type PlayerState = {
hasInitialized: boolean; hasInitialized: boolean;
leftControlHovering: boolean; leftControlHovering: boolean;
hasPlayedOnce: boolean; hasPlayedOnce: boolean;
seasonData: {
isSeries: boolean;
current?: {
episode: number;
season: number;
};
title?: string;
};
error: null | { error: null | {
name: string; name: string;
description: string; description: string;
@ -47,6 +55,9 @@ export const initialPlayerState: PlayerContext = {
leftControlHovering: false, leftControlHovering: false,
hasPlayedOnce: false, hasPlayedOnce: false,
error: null, error: null,
seasonData: {
isSeries: false,
},
...initialControls, ...initialControls,
}; };

View File

@ -35,19 +35,22 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
<span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" /> <span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" />
) : null} ) : null}
{props.media ? ( {props.media ? (
<span className="flex items-center space-x-2 text-white"> <span className="flex items-center text-white">
<span>{props.media.title}</span> <span>{props.media.title}</span>
<IconPatch
clickable
transparent
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
onClick={() =>
props.media && setItemBookmark(props.media, !isBookmarked)
}
/>
</span> </span>
) : null} ) : null}
</p> </p>
{props.media ? (
<IconPatch
clickable
transparent
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
className="ml-2 text-white"
onClick={() =>
props.media && setItemBookmark(props.media, !isBookmarked)
}
/>
) : null}
</div> </div>
<BrandPill /> <BrandPill />
</div> </div>

View File

@ -26,10 +26,6 @@ if (key) {
// - safari fullscreen will make video overlap player controls // - safari fullscreen will make video overlap player controls
// - safari progress bar is fucked (video doesnt change time but video.currentTime does change) // - safari progress bar is fucked (video doesnt change time but video.currentTime does change)
// TODO optional todos:
// - shortcuts when player is active
// - improve seekables (if possible)
// TODO stuff to test: // TODO stuff to test:
// - browser: firefox, chrome, edge, safari desktop // - browser: firefox, chrome, edge, safari desktop
// - phones: android firefox, android chrome, iphone safari // - phones: android firefox, android chrome, iphone safari

View File

@ -6,6 +6,7 @@ import {
useCallback, useCallback,
useContext, useContext,
useMemo, useMemo,
useRef,
useState, useState,
} from "react"; } from "react";
import { VideoProgressStore } from "./store"; import { VideoProgressStore } from "./store";
@ -180,15 +181,20 @@ export function useWatchedItem(meta: DetailedMeta | null) {
() => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id),
[watched, meta] [watched, meta]
); );
const lastCommitedTime = useRef([0, 0]);
const callback = useCallback( const callback = useCallback(
(progress: number, total: number) => { (progress: number, total: number) => {
if (meta) { // TODO add series support
// TODO add series support const hasChanged =
lastCommitedTime.current[0] !== progress ||
lastCommitedTime.current[1] !== total;
if (meta && hasChanged) {
lastCommitedTime.current = [progress, total];
updateProgress({ meta: meta.meta }, progress, total); updateProgress({ meta: meta.meta }, progress, total);
} }
}, },
[updateProgress, meta] [meta, updateProgress]
); );
return { updateProgress: callback, watchedItem: item }; return { updateProgress: callback, watchedItem: item };

View File

@ -1,5 +1,5 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWStream } from "@/backend/helpers/streams"; import { MWStream } from "@/backend/helpers/streams";
import { SelectedMediaData, useScrape } from "@/hooks/useScrape"; import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
@ -15,6 +15,7 @@ import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { useWatchedItem } from "@/state/watched"; import { useWatchedItem } from "@/state/watched";
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
import { ShowControl } from "@/components/video/controls/ShowControl";
import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaFetchErrorView } from "./MediaErrorView";
import { MediaScrapeLog } from "./MediaScrapeLog"; import { MediaScrapeLog } from "./MediaScrapeLog";
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
@ -81,6 +82,37 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
); );
} }
interface MediaViewPlayerProps {
meta: DetailedMeta;
stream: MWStream;
}
export function MediaViewPlayer(props: MediaViewPlayerProps) {
const goBack = useGoBack();
const { updateProgress, watchedItem } = useWatchedItem(props.meta);
const firstStartTime = useRef(watchedItem?.progress);
useEffect(() => {
firstStartTime.current = watchedItem?.progress;
// only want it to change when stream changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.stream]);
return (
<div className="h-screen w-screen">
<DecoratedVideoPlayer media={props.meta.meta} onGoBack={goBack} autoPlay>
<SourceControl
source={props.stream.streamUrl}
type={props.stream.type}
/>
<ProgressListenerControl
startAt={firstStartTime.current}
onProgress={updateProgress}
/>
<ShowControl series={{ episode: 5, season: 2 }} title="hello world" />
</DecoratedVideoPlayer>
</div>
);
}
export function MediaView() { export function MediaView() {
const params = useParams<{ media: string }>(); const params = useParams<{ media: string }>();
const goBack = useGoBack(); const goBack = useGoBack();
@ -101,8 +133,6 @@ export function MediaView() {
}); });
const [stream, setStream] = useState<MWStream | null>(null); const [stream, setStream] = useState<MWStream | null>(null);
const { updateProgress, watchedItem } = useWatchedItem(meta);
useEffect(() => { useEffect(() => {
exec(params.media).then((v) => { exec(params.media).then((v) => {
setMeta(v ?? null); setMeta(v ?? null);
@ -137,15 +167,5 @@ export function MediaView() {
); );
// show stream once we have a stream // show stream once we have a stream
return ( return <MediaViewPlayer meta={meta} stream={stream} />;
<div className="h-screen w-screen">
<DecoratedVideoPlayer media={meta.meta} onGoBack={goBack} autoPlay>
<SourceControl source={stream.streamUrl} type={stream.type} />
<ProgressListenerControl
startAt={watchedItem?.progress}
onProgress={updateProgress}
/>
</DecoratedVideoPlayer>
</div>
);
} }