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

View File

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

View File

@ -7,7 +7,6 @@ interface Props {
onProgress?: (time: number, duration: number) => void;
}
// TODO fix infinite loops
export function ProgressListenerControl(props: Props) {
const { videoState } = useVideoPlayerState();
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 { getStoredVolume, setStoredVolume } from "./volumeStore";
interface ShowData {
current?: {
episode: number;
season: number;
};
isSeries: boolean;
title?: string;
}
export interface PlayerControls {
play(): void;
pause(): void;
@ -21,6 +30,7 @@ export interface PlayerControls {
setSeeking(active: boolean): void;
setLeftControlsHover(hovering: boolean): void;
initPlayer(sourceUrl: string, sourceType: MWStreamType): void;
setShowData(data: ShowData): void;
}
export const initialControls: PlayerControls = {
@ -33,6 +43,7 @@ export const initialControls: PlayerControls = {
setSeeking: () => null,
setLeftControlsHover: () => null,
initPlayer: () => null,
setShowData: () => null,
};
export function populateControls(
@ -105,6 +116,9 @@ export function populateControls(
setLeftControlsHover(hovering) {
update((s) => ({ ...s, leftControlHovering: hovering }));
},
setShowData(data) {
update((s) => ({ ...s, seasonData: data }));
},
initPlayer(sourceUrl: string, sourceType: MWStreamType) {
this.setVolume(getStoredVolume());

View File

@ -23,6 +23,14 @@ export type PlayerState = {
hasInitialized: boolean;
leftControlHovering: boolean;
hasPlayedOnce: boolean;
seasonData: {
isSeries: boolean;
current?: {
episode: number;
season: number;
};
title?: string;
};
error: null | {
name: string;
description: string;
@ -47,6 +55,9 @@ export const initialPlayerState: PlayerContext = {
leftControlHovering: false,
hasPlayedOnce: false,
error: null,
seasonData: {
isSeries: false,
},
...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" />
) : null}
{props.media ? (
<span className="flex items-center space-x-2 text-white">
<span className="flex items-center text-white">
<span>{props.media.title}</span>
<IconPatch
clickable
transparent
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
onClick={() =>
props.media && setItemBookmark(props.media, !isBookmarked)
}
/>
</span>
) : null}
</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>
<BrandPill />
</div>

View File

@ -26,10 +26,6 @@ if (key) {
// - safari fullscreen will make video overlap player controls
// - 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:
// - browser: firefox, chrome, edge, safari desktop
// - phones: android firefox, android chrome, iphone safari

View File

@ -6,6 +6,7 @@ import {
useCallback,
useContext,
useMemo,
useRef,
useState,
} from "react";
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, meta]
);
const lastCommitedTime = useRef([0, 0]);
const callback = useCallback(
(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, updateProgress]
);
return { updateProgress: callback, watchedItem: item };

View File

@ -1,5 +1,5 @@
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWStream } from "@/backend/helpers/streams";
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
@ -15,6 +15,7 @@ import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { useWatchedItem } from "@/state/watched";
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
import { ShowControl } from "@/components/video/controls/ShowControl";
import { MediaFetchErrorView } from "./MediaErrorView";
import { MediaScrapeLog } from "./MediaScrapeLog";
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() {
const params = useParams<{ media: string }>();
const goBack = useGoBack();
@ -101,8 +133,6 @@ export function MediaView() {
});
const [stream, setStream] = useState<MWStream | null>(null);
const { updateProgress, watchedItem } = useWatchedItem(meta);
useEffect(() => {
exec(params.media).then((v) => {
setMeta(v ?? null);
@ -137,15 +167,5 @@ export function MediaView() {
);
// show stream once we have a stream
return (
<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>
);
return <MediaViewPlayer meta={meta} stream={stream} />;
}