From d28e6e6735413c541c7d5bd21002b829384c6d66 Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Tue, 10 Jan 2023 21:18:10 +0100 Subject: [PATCH] implement video player on mediapage --- src/components/media/VideoPlayer.tsx | 109 ------------------ src/components/video/DecoratedVideoPlayer.tsx | 11 +- src/components/video/controls/TimeControl.tsx | 2 +- .../video/parts/VideoPlayerHeader.tsx | 27 +++-- src/views/MediaView.tsx | 47 +++++--- src/views/TestView.tsx | 8 +- 6 files changed, 62 insertions(+), 142 deletions(-) delete mode 100644 src/components/media/VideoPlayer.tsx diff --git a/src/components/media/VideoPlayer.tsx b/src/components/media/VideoPlayer.tsx deleted file mode 100644 index 0009922e..00000000 --- a/src/components/media/VideoPlayer.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { ReactElement, useEffect, useRef, useState } from "react"; -import Hls from "hls.js"; -import { IconPatch } from "@/components/buttons/IconPatch"; -import { Icons } from "@/components/Icon"; -import { Loading } from "@/components/layout/Loading"; -import { MWMediaCaption, MWMediaStream } from "@/providers"; - -export interface VideoPlayerProps { - source: MWMediaStream; - captions: MWMediaCaption[]; - startAt?: number; - onProgress?: (event: ProgressEvent) => void; -} - -export function SkeletonVideoPlayer(props: { error?: boolean }) { - return ( -
- {props.error ? ( -
- -

Couldn't get your stream

-
- ) : ( -
- -

Getting your stream...

-
- )} -
- ); -} - -export function VideoPlayer(props: VideoPlayerProps) { - const videoRef = useRef(null); - const [hasErrored, setErrored] = useState(false); - const [isLoading, setLoading] = useState(true); - const showVideo = !isLoading && !hasErrored; - const mustUseHls = props.source.type === "m3u8"; - - // reset if stream url changes - useEffect(() => { - setLoading(true); - setErrored(false); - - // hls support - if (mustUseHls) { - if (!videoRef.current) return; - - if (!Hls.isSupported()) { - setLoading(false); - setErrored(true); - return; - } - - const hls = new Hls(); - - if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { - videoRef.current.src = props.source.url; - return; - } - - hls.attachMedia(videoRef.current); - hls.loadSource(props.source.url); - - hls.on(Hls.Events.ERROR, (event, data) => { - setErrored(true); - console.error(data); - }); - } - }, [props.source.url, videoRef, mustUseHls]); - - let skeletonUi: null | ReactElement = null; - if (hasErrored) { - skeletonUi = ; - } else if (isLoading) { - skeletonUi = ; - } - - return ( - <> - {skeletonUi} - - - ); -} diff --git a/src/components/video/DecoratedVideoPlayer.tsx b/src/components/video/DecoratedVideoPlayer.tsx index 222fd920..e1582d5c 100644 --- a/src/components/video/DecoratedVideoPlayer.tsx +++ b/src/components/video/DecoratedVideoPlayer.tsx @@ -12,6 +12,11 @@ import { VideoPlayerHeader } from "./parts/VideoPlayerHeader"; import { useVideoPlayerState } from "./VideoContext"; import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer"; +interface DecoratedVideoPlayerProps { + title?: string; + onGoBack?: () => void; +} + function LeftSideControls() { const { videoState } = useVideoPlayerState(); @@ -35,7 +40,9 @@ function LeftSideControls() { ); } -export function DecoratedVideoPlayer(props: VideoPlayerProps) { +export function DecoratedVideoPlayer( + props: VideoPlayerProps & DecoratedVideoPlayerProps +) { const top = useRef(null); const bottom = useRef(null); const [show, setShow] = useState(false); @@ -98,7 +105,7 @@ export function DecoratedVideoPlayer(props: VideoPlayerProps) { ref={top} className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2" > - + diff --git a/src/components/video/controls/TimeControl.tsx b/src/components/video/controls/TimeControl.tsx index aeeb99d4..42e78329 100644 --- a/src/components/video/controls/TimeControl.tsx +++ b/src/components/video/controls/TimeControl.tsx @@ -17,7 +17,7 @@ function formatSeconds(secs: number, showHours = false): string { const minutes = time % 60; time /= 60; - const hours = minutes % 60; + const hours = time % 60; if (!showHours) return `${Math.round(minutes).toString()}:${Math.round(seconds) diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index d0cf55f2..83138b19 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -2,24 +2,31 @@ import { Icon, Icons } from "@/components/Icon"; import { BrandPill } from "@/components/layout/BrandPill"; interface VideoPlayerHeaderProps { - title: string; + title?: string; onClick?: () => void; } export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { + const showDivider = props.title || props.onClick; return (

- - - Back to home - - - {props.title} + {props.onClick ? ( + + + Back to home + + ) : null} + {showDivider ? ( + + ) : null} + {props.title ? ( + {props.title} + ) : null}

diff --git a/src/views/MediaView.tsx b/src/views/MediaView.tsx index 88fe66df..b82332e1 100644 --- a/src/views/MediaView.tsx +++ b/src/views/MediaView.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect, useState } from "react"; +import { ReactElement, useCallback, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { IconPatch } from "@/components/buttons/IconPatch"; @@ -6,10 +6,8 @@ import { Icons } from "@/components/Icon"; import { Navigation } from "@/components/layout/Navigation"; import { Paper } from "@/components/layout/Paper"; import { LoadingSeasons, Seasons } from "@/components/layout/Seasons"; -import { - SkeletonVideoPlayer, - VideoPlayer, -} from "@/components/media/VideoPlayer"; +import { SkeletonVideoPlayer } from "@/components/media/VideoPlayer"; +import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; import { ArrowLink } from "@/components/text/ArrowLink"; import { DotList } from "@/components/text/DotList"; import { Title } from "@/components/text/Title"; @@ -30,6 +28,8 @@ import { useBookmarkContext, } from "@/state/bookmark"; import { getWatchedFromPortable, useWatchedContext } from "@/state/watched"; +import { SourceControl } from "@/components/video/controls/SourceControl"; +import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; import { NotFoundChecks } from "./notfound/NotFoundChecks"; interface StyledMediaViewProps { @@ -38,28 +38,37 @@ interface StyledMediaViewProps { } function StyledMediaView(props: StyledMediaViewProps) { + const reactHistory = useHistory(); const watchedStore = useWatchedContext(); const startAtTime: number | undefined = getWatchedFromPortable( watchedStore.watched.items, props.media )?.progress; - function updateProgress(e: Event) { - if (!props.media) return; - const el: HTMLVideoElement = e.currentTarget as HTMLVideoElement; - if (el.currentTime <= 30) { - return; // Don't update stored progress if less than 30s into the video - } - watchedStore.updateProgress(props.media, el.currentTime, el.duration); - } + const updateProgress = useCallback( + (time: number, duration: number) => { + // Don't update stored progress if less than 30s into the video + if (time <= 30) return; + watchedStore.updateProgress(props.media, time, duration); + }, + [props, watchedStore] + ); + + const goBack = useCallback(() => { + if (reactHistory.action !== "POP") reactHistory.goBack(); + else reactHistory.push("/"); + }, [reactHistory]); return ( - updateProgress(e)} - startAt={startAtTime} - /> +
+ + + + +
); } diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx index 5dd391f3..eaac96b6 100644 --- a/src/views/TestView.tsx +++ b/src/views/TestView.tsx @@ -10,12 +10,18 @@ import { useCallback, useState } from "react"; // - captions // - mobile UI // - safari fullscreen will make video overlap player controls -// - safari progress bar is fucked +// - 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 +// - devices: ipadOS +// - features: HLS, error handling + export function TestView() { const [show, setShow] = useState(true); const handleClick = useCallback(() => {