mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
Backdrop + improved seeking
This commit is contained in:
parent
b43b8b19e4
commit
098f6af0ae
6 changed files with 124 additions and 28 deletions
24
src/components/video/DecoratedVideoPlayer.tsx
Normal file
24
src/components/video/DecoratedVideoPlayer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
53
src/components/video/controls/BackdropControl.tsx
Normal file
53
src/components/video/controls/BackdropControl.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -12,3 +12,7 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-no-select] {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue