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 { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext";
interface VideoPlayerProps {
export interface VideoPlayerProps {
autoPlay?: boolean;
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";
export function ProgressControl() {
const { videoState } = useVideoPlayerState();
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) *
100
).toFixed(2)}%`;
if (mouseDown) watchProgress = `${progress}%`;
const bufferProgress = `${(
(videoState.buffered / videoState.duration) *
100
).toFixed(2)}%`;
const handleClick = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
useEffect(() => {
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;
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, 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 (
<div
ref={ref}
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
className="absolute inset-y-0 left-0 bg-denim-700 opacity-50"

View File

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