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,
+  };
+}