Backdrop + improved seeking

This commit is contained in:
Jelle van Snik 2023-01-08 22:29:38 +01:00
parent b43b8b19e4
commit 098f6af0ae
6 changed files with 124 additions and 28 deletions

View File

@ -0,0 +1,24 @@
import { BackdropControl } from "./controls/BackdropControl";
import { FullscreenControl } from "./controls/FullscreenControl";
import { LoadingControl } from "./controls/LoadingControl";
import { PauseControl } from "./controls/PauseControl";
import { ProgressControl } from "./controls/ProgressControl";
import { TimeControl } from "./controls/TimeControl";
import { VolumeControl } from "./controls/VolumeControl";
import { VideoPlayer, VideoPlayerProps } from "./VideoPlayer";
export function DecoratedVideoPlayer(props: VideoPlayerProps) {
return (
<VideoPlayer autoPlay={props.autoPlay}>
<BackdropControl>
<PauseControl />
<FullscreenControl />
<ProgressControl />
<VolumeControl />
<LoadingControl />
<TimeControl />
</BackdropControl>
{props.children}
</VideoPlayer>
);
}

View File

@ -1,7 +1,7 @@
import { forwardRef, useContext, useRef } from "react"; import { forwardRef, useContext, useRef } from "react";
import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext";
interface VideoPlayerProps { export interface VideoPlayerProps {
autoPlay?: boolean; autoPlay?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
} }

View File

@ -0,0 +1,53 @@
import { useCallback, useRef, useState } from "react";
import { useVideoPlayerState } from "../VideoContext";
interface BackdropControlProps {
children?: React.ReactNode;
}
export function BackdropControl(props: BackdropControlProps) {
const { videoState } = useVideoPlayerState();
const [moved, setMoved] = useState(false);
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseMove = useCallback(() => {
setMoved(true);
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
setMoved(false);
timeout.current = null;
}, 3000);
}, [timeout, setMoved]);
const handleClick = useCallback(() => {
if (videoState.isPlaying) videoState.pause();
else videoState.play();
}, [videoState]);
const showUI = moved || videoState.isPaused;
return (
<div
className={`absolute inset-0 ${!showUI ? "cursor-none" : ""}`}
onMouseMove={handleMouseMove}
onClick={handleClick}
>
<div
className={`absolute inset-0 bg-black bg-opacity-20 transition-opacity duration-200 ${
!showUI ? "!opacity-0" : ""
}`}
/>
<div
className={`absolute inset-x-0 bottom-0 h-[30%] bg-gradient-to-t from-black to-transparent opacity-75 transition-opacity duration-200 ${
!showUI ? "!opacity-0" : ""
}`}
/>
<div
className={`absolute inset-x-0 top-0 h-[30%] bg-gradient-to-b from-black to-transparent opacity-75 transition-opacity duration-200 ${
!showUI ? "!opacity-0" : ""
}`}
/>
<div className="absolute inset-0">{showUI ? props.children : null}</div>
</div>
);
}

View File

@ -1,34 +1,61 @@
import { useCallback, useRef } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useVideoPlayerState } from "../VideoContext"; import { useVideoPlayerState } from "../VideoContext";
export function ProgressControl() { export function ProgressControl() {
const { videoState } = useVideoPlayerState(); const { videoState } = useVideoPlayerState();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [mouseDown, setMouseDown] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0);
const watchProgress = `${( let watchProgress = `${(
(videoState.time / videoState.duration) * (videoState.time / videoState.duration) *
100 100
).toFixed(2)}%`; ).toFixed(2)}%`;
if (mouseDown) watchProgress = `${progress}%`;
const bufferProgress = `${( const bufferProgress = `${(
(videoState.buffered / videoState.duration) * (videoState.buffered / videoState.duration) *
100 100
).toFixed(2)}%`; ).toFixed(2)}%`;
const handleClick = useCallback( useEffect(() => {
(e: React.MouseEvent<HTMLElement>) => { function mouseMove(ev: MouseEvent) {
if (!mouseDown || !ref.current) return;
const rect = ref.current.getBoundingClientRect();
const pos = ((ev.pageX - rect.left) / ref.current.offsetWidth) * 100;
setProgress(pos);
}
function mouseUp(ev: MouseEvent) {
if (!mouseDown) return;
setMouseDown(false);
document.body.removeAttribute("data-no-select");
if (!ref.current) return; if (!ref.current) return;
const rect = ref.current.getBoundingClientRect(); const rect = ref.current.getBoundingClientRect();
const pos = (e.pageX - rect.left) / ref.current.offsetWidth; const pos = (ev.pageX - rect.left) / ref.current.offsetWidth;
videoState.setTime(pos * videoState.duration); videoState.setTime(pos * videoState.duration);
}, }
[videoState, ref]
); document.addEventListener("mousemove", mouseMove);
document.addEventListener("mouseup", mouseUp);
return () => {
document.removeEventListener("mousemove", mouseMove);
document.removeEventListener("mouseup", mouseUp);
};
}, [mouseDown, videoState]);
const handleMouseDown = useCallback(() => {
setMouseDown(true);
document.body.setAttribute("data-no-select", "true");
}, []);
return ( return (
<div <div
ref={ref} ref={ref}
className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-denim-100" className="relative m-1 my-4 h-4 w-48 overflow-hidden rounded-full border border-white bg-denim-100"
onClick={handleClick} onMouseDown={handleMouseDown}
> >
<div <div
className="absolute inset-y-0 left-0 bg-denim-700 opacity-50" className="absolute inset-y-0 left-0 bg-denim-700 opacity-50"

View File

@ -12,3 +12,7 @@ body {
min-height: 100vh; min-height: 100vh;
width: 100%; width: 100%;
} }
body[data-no-select] {
user-select: none;
}

View File

@ -1,28 +1,22 @@
import { FullscreenControl } from "@/components/video/controls/FullscreenControl";
import { LoadingControl } from "@/components/video/controls/LoadingControl";
import { PauseControl } from "@/components/video/controls/PauseControl";
import { ProgressControl } from "@/components/video/controls/ProgressControl";
import { SourceControl } from "@/components/video/controls/SourceControl"; import { SourceControl } from "@/components/video/controls/SourceControl";
import { TimeControl } from "@/components/video/controls/TimeControl"; import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { VolumeControl } from "@/components/video/controls/VolumeControl";
import { VideoPlayer } from "@/components/video/VideoPlayer";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
// TODO video todos: // TODO video todos:
// - make pretty // - make pretty
// - better seeking
// - improve seekables // - improve seekables
// - error handling // - error handling
// - middle pause button + click to pause // - middle pause button
// - improve pausing while seeking/buffering // - improve pausing while seeking/buffering
// - captions // - captions
// - backdrop better click handling
// - IOS support: (no volume, fullscreen video element instead of wrapper) // - IOS support: (no volume, fullscreen video element instead of wrapper)
// - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) ) // - IpadOS support: (fullscreen video wrapper should work, see (lookmovie.io) )
// - HLS support: feature detection otherwise use HLS.js // - HLS support: feature detection otherwise use HLS.js
export function TestView() { export function TestView() {
const [show, setShow] = useState(false); const [show, setShow] = useState(true);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setShow((v) => !v); setShow((v) => !v);
}, [setShow]); }, [setShow]);
@ -33,15 +27,9 @@ export function TestView() {
return ( return (
<div className="w-[40rem] max-w-full"> <div className="w-[40rem] max-w-full">
<VideoPlayer autoPlay> <DecoratedVideoPlayer>
<PauseControl />
<FullscreenControl />
<ProgressControl />
<VolumeControl />
<LoadingControl />
<TimeControl />
<SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" /> <SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" />
</VideoPlayer> </DecoratedVideoPlayer>
</div> </div>
); );
} }