From 3a67d50f425d71ab9f07da513526d0d20acdadd6 Mon Sep 17 00:00:00 2001 From: Jelle van Snik <jellevs@gmail.com> Date: Sun, 8 Jan 2023 15:37:16 +0100 Subject: [PATCH] video player starter --- src/components/video/VideoContext.tsx | 72 +++++++------------ src/components/video/VideoPlayer.tsx | 42 ++--------- .../video/controls/FullscreenControl.tsx | 41 +++++------ .../video/controls/PauseControl.tsx | 26 ++----- src/components/video/hooks/controlVideo.ts | 20 ++++++ src/components/video/hooks/useVideoPlayer.ts | 55 ++++++++++++++ 6 files changed, 137 insertions(+), 119 deletions(-) create mode 100644 src/components/video/hooks/controlVideo.ts create mode 100644 src/components/video/hooks/useVideoPlayer.ts diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx index dd8301d9..bc1bedea 100644 --- a/src/components/video/VideoContext.tsx +++ b/src/components/video/VideoContext.tsx @@ -1,36 +1,30 @@ import React, { createContext, MutableRefObject, + useContext, useEffect, useReducer, } from "react"; +import { + initialPlayerState, + PlayerState, + useVideoPlayer, +} from "./hooks/useVideoPlayer"; interface VideoPlayerContextType { - source: null | string; - playerWrapper: HTMLDivElement | null; - player: HTMLVideoElement | null; - controlState: "paused" | "playing"; - fullscreen: boolean; + source: string | null; + state: PlayerState; } -const initial = ( - player: HTMLVideoElement | null = null, - wrapper: HTMLDivElement | null = null -): VideoPlayerContextType => ({ +const initial: VideoPlayerContextType = { source: null, - playerWrapper: wrapper, - player, - controlState: "paused", - fullscreen: false, -}); + state: initialPlayerState, +}; type VideoPlayerContextAction = | { type: "SET_SOURCE"; url: string } - | { type: "CONTROL"; do: "PAUSE" | "PLAY"; soft?: boolean } - | { type: "FULLSCREEN"; do: "ENTER" | "EXIT"; soft?: boolean } | { type: "UPDATE_PLAYER"; - player: HTMLVideoElement | null; - playerWrapper: HTMLDivElement | null; + state: PlayerState; }; function videoPlayerContextReducer( @@ -42,35 +36,16 @@ function videoPlayerContextReducer( video.source = action.url; return video; } - if (action.type === "CONTROL") { - if (action.do === "PAUSE") video.controlState = "paused"; - else if (action.do === "PLAY") video.controlState = "playing"; - if (action.soft) return video; - - if (action.do === "PAUSE") video.player?.pause(); - else if (action.do === "PLAY") video.player?.play(); - return video; - } if (action.type === "UPDATE_PLAYER") { - video.player = action.player; - video.playerWrapper = action.playerWrapper; - return video; - } - if (action.type === "FULLSCREEN") { - video.fullscreen = action.do === "ENTER"; - if (action.soft) return video; - - if (action.do === "ENTER") video.playerWrapper?.requestFullscreen(); - else document.exitFullscreen(); + video.state = action.state; return video; } return original; } -export const VideoPlayerContext = createContext<VideoPlayerContextType>( - initial() -); +export const VideoPlayerContext = + createContext<VideoPlayerContextType>(initial); export const VideoPlayerDispatchContext = createContext< React.Dispatch<VideoPlayerContextAction> >(null as any); @@ -78,20 +53,19 @@ export const VideoPlayerDispatchContext = createContext< export function VideoPlayerContextProvider(props: { children: React.ReactNode; player: MutableRefObject<HTMLVideoElement | null>; - playerWrapper: MutableRefObject<HTMLDivElement | null>; }) { + const { playerState } = useVideoPlayer(props.player); const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>( videoPlayerContextReducer, - initial() + initial ); useEffect(() => { dispatch({ type: "UPDATE_PLAYER", - player: props.player.current, - playerWrapper: props.playerWrapper.current, + state: playerState, }); - }, [props.player, props.playerWrapper]); + }, [playerState]); return ( <VideoPlayerContext.Provider value={videoData}> @@ -101,3 +75,11 @@ export function VideoPlayerContextProvider(props: { </VideoPlayerContext.Provider> ); } + +export function useVideoPlayerState() { + const { state } = useContext(VideoPlayerContext); + + return { + videoState: state, + }; +} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx index d2395bb7..d1b7f341 100644 --- a/src/components/video/VideoPlayer.tsx +++ b/src/components/video/VideoPlayer.tsx @@ -1,37 +1,15 @@ -import { forwardRef, useCallback, useContext, useEffect, useRef } from "react"; -import { - VideoPlayerContext, - VideoPlayerContextProvider, - VideoPlayerDispatchContext, -} from "./VideoContext"; +import { forwardRef, useContext, useRef } from "react"; +import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext"; interface VideoPlayerProps { children?: React.ReactNode; } -const VideoPlayerInternals = forwardRef<HTMLVideoElement>((props, ref) => { +const VideoPlayerInternals = forwardRef<HTMLVideoElement>((_, ref) => { const video = useContext(VideoPlayerContext); - const dispatch = useContext(VideoPlayerDispatchContext); - - const onPlay = useCallback(() => { - dispatch({ - type: "CONTROL", - do: "PLAY", - soft: true, - }); - }, [dispatch]); - const onPause = useCallback(() => { - dispatch({ - type: "CONTROL", - do: "PAUSE", - soft: true, - }); - }, [dispatch]); - - useEffect(() => {}, []); return ( - <video ref={ref} onPlay={onPlay} onPause={onPause} controls> + <video controls ref={ref}> {video.source ? <source src={video.source} type="video/mp4" /> : null} </video> ); @@ -39,17 +17,11 @@ const VideoPlayerInternals = forwardRef<HTMLVideoElement>((props, ref) => { export function VideoPlayer(props: VideoPlayerProps) { const playerRef = useRef<HTMLVideoElement | null>(null); - const playerWrapperRef = useRef<HTMLDivElement | null>(null); return ( - <VideoPlayerContextProvider - player={playerRef} - playerWrapper={playerWrapperRef} - > - <div ref={playerWrapperRef} className="bg-blue-900"> - <VideoPlayerInternals ref={playerRef} /> - {props.children} - </div> + <VideoPlayerContextProvider player={playerRef}> + <VideoPlayerInternals ref={playerRef} /> + {props.children} </VideoPlayerContextProvider> ); } diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx index 332df675..62c9e9f3 100644 --- a/src/components/video/controls/FullscreenControl.tsx +++ b/src/components/video/controls/FullscreenControl.tsx @@ -1,26 +1,27 @@ -import { useCallback, useContext } from "react"; -import { - VideoPlayerContext, - VideoPlayerDispatchContext, -} from "../VideoContext"; +// import { useCallback, useContext } from "react"; +// import { +// VideoPlayerContext, +// VideoPlayerDispatchContext, +// } from "../VideoContext"; export function FullscreenControl() { - const dispatch = useContext(VideoPlayerDispatchContext); - const video = useContext(VideoPlayerContext); + return <p>Hello world</p>; + // const dispatch = useContext(VideoPlayerDispatchContext); + // const video = useContext(VideoPlayerContext); - const handleClick = useCallback(() => { - dispatch({ - type: "FULLSCREEN", - do: video.fullscreen ? "EXIT" : "ENTER", - }); - }, [video, dispatch]); + // const handleClick = useCallback(() => { + // dispatch({ + // type: "FULLSCREEN", + // do: video.fullscreen ? "EXIT" : "ENTER", + // }); + // }, [video, dispatch]); - let text = "not fullscreen"; - if (video.fullscreen) text = "in fullscreen"; + // let text = "not fullscreen"; + // if (video.fullscreen) text = "in fullscreen"; - return ( - <button type="button" onClick={handleClick}> - {text} - </button> - ); + // return ( + // <button type="button" onClick={handleClick}> + // {text} + // </button> + // ); } diff --git a/src/components/video/controls/PauseControl.tsx b/src/components/video/controls/PauseControl.tsx index 9ea9bcb6..3b4c408d 100644 --- a/src/components/video/controls/PauseControl.tsx +++ b/src/components/video/controls/PauseControl.tsx @@ -1,28 +1,16 @@ -import { useCallback, useContext } from "react"; -import { - VideoPlayerContext, - VideoPlayerDispatchContext, -} from "../VideoContext"; +import { useCallback } from "react"; +import { useVideoPlayerState } from "../VideoContext"; export function PauseControl() { - const dispatch = useContext(VideoPlayerDispatchContext); - const video = useContext(VideoPlayerContext); + const { videoState } = useVideoPlayerState(); const handleClick = useCallback(() => { - if (video.controlState === "playing") - dispatch({ - type: "CONTROL", - do: "PAUSE", - }); - else if (video.controlState === "paused") - dispatch({ - type: "CONTROL", - do: "PLAY", - }); - }, [video, dispatch]); + if (videoState?.isPlaying) videoState.pause(); + else videoState.play(); + }, [videoState]); let text = "paused"; - if (video.controlState === "playing") text = "playing"; + if (videoState?.isPlaying) text = "playing"; return ( <button type="button" onClick={handleClick}> diff --git a/src/components/video/hooks/controlVideo.ts b/src/components/video/hooks/controlVideo.ts new file mode 100644 index 00000000..4b980526 --- /dev/null +++ b/src/components/video/hooks/controlVideo.ts @@ -0,0 +1,20 @@ +export interface PlayerControls { + play(): void; + pause(): void; +} + +export const initialControls: PlayerControls = { + play: () => null, + pause: () => null, +}; + +export function populateControls(player: HTMLVideoElement): PlayerControls { + return { + play() { + player.play(); + }, + pause() { + player.pause(); + }, + }; +} diff --git a/src/components/video/hooks/useVideoPlayer.ts b/src/components/video/hooks/useVideoPlayer.ts new file mode 100644 index 00000000..2e513951 --- /dev/null +++ b/src/components/video/hooks/useVideoPlayer.ts @@ -0,0 +1,55 @@ +import React, { MutableRefObject, useEffect, useState } from "react"; +import { + initialControls, + PlayerControls, + populateControls, +} from "./controlVideo"; + +export type PlayerState = { + isPlaying: boolean; + isPaused: boolean; +} & PlayerControls; + +export const initialPlayerState = { + isPlaying: false, + isPaused: true, + ...initialControls, +}; + +type SetPlayer = (s: React.SetStateAction<PlayerState>) => void; + +function readState(player: HTMLVideoElement, update: SetPlayer) { + const state = { + ...initialPlayerState, + }; + state.isPaused = player.paused; + state.isPlaying = !player.paused; + + update(state); +} + +function registerListeners(player: HTMLVideoElement, update: SetPlayer) { + player.addEventListener("pause", () => { + update((s) => ({ ...s, isPaused: true, isPlaying: false })); + }); + player.addEventListener("play", () => { + update((s) => ({ ...s, isPaused: false, isPlaying: true })); + }); +} + +export function useVideoPlayer(ref: MutableRefObject<HTMLVideoElement | null>) { + const [state, setState] = useState(initialPlayerState); + + useEffect(() => { + const player = ref.current; + if (player) { + readState(player, setState); + registerListeners(player, setState); + setState((s) => ({ ...s, ...populateControls(player) })); + } + }, [ref]); + + return { + playerState: state, + }; +}