video player starter

This commit is contained in:
Jelle van Snik 2023-01-08 15:37:16 +01:00
parent eeaa4d7571
commit 3a67d50f42
6 changed files with 137 additions and 119 deletions

View file

@ -1,36 +1,30 @@
import React, { import React, {
createContext, createContext,
MutableRefObject, MutableRefObject,
useContext,
useEffect, useEffect,
useReducer, useReducer,
} from "react"; } from "react";
import {
initialPlayerState,
PlayerState,
useVideoPlayer,
} from "./hooks/useVideoPlayer";
interface VideoPlayerContextType { interface VideoPlayerContextType {
source: null | string; source: string | null;
playerWrapper: HTMLDivElement | null; state: PlayerState;
player: HTMLVideoElement | null;
controlState: "paused" | "playing";
fullscreen: boolean;
} }
const initial = ( const initial: VideoPlayerContextType = {
player: HTMLVideoElement | null = null,
wrapper: HTMLDivElement | null = null
): VideoPlayerContextType => ({
source: null, source: null,
playerWrapper: wrapper, state: initialPlayerState,
player, };
controlState: "paused",
fullscreen: false,
});
type VideoPlayerContextAction = type VideoPlayerContextAction =
| { type: "SET_SOURCE"; url: string } | { type: "SET_SOURCE"; url: string }
| { type: "CONTROL"; do: "PAUSE" | "PLAY"; soft?: boolean }
| { type: "FULLSCREEN"; do: "ENTER" | "EXIT"; soft?: boolean }
| { | {
type: "UPDATE_PLAYER"; type: "UPDATE_PLAYER";
player: HTMLVideoElement | null; state: PlayerState;
playerWrapper: HTMLDivElement | null;
}; };
function videoPlayerContextReducer( function videoPlayerContextReducer(
@ -42,35 +36,16 @@ function videoPlayerContextReducer(
video.source = action.url; video.source = action.url;
return video; 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") { if (action.type === "UPDATE_PLAYER") {
video.player = action.player; video.state = action.state;
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();
return video; return video;
} }
return original; return original;
} }
export const VideoPlayerContext = createContext<VideoPlayerContextType>( export const VideoPlayerContext =
initial() createContext<VideoPlayerContextType>(initial);
);
export const VideoPlayerDispatchContext = createContext< export const VideoPlayerDispatchContext = createContext<
React.Dispatch<VideoPlayerContextAction> React.Dispatch<VideoPlayerContextAction>
>(null as any); >(null as any);
@ -78,20 +53,19 @@ export const VideoPlayerDispatchContext = createContext<
export function VideoPlayerContextProvider(props: { export function VideoPlayerContextProvider(props: {
children: React.ReactNode; children: React.ReactNode;
player: MutableRefObject<HTMLVideoElement | null>; player: MutableRefObject<HTMLVideoElement | null>;
playerWrapper: MutableRefObject<HTMLDivElement | null>;
}) { }) {
const { playerState } = useVideoPlayer(props.player);
const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>( const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>(
videoPlayerContextReducer, videoPlayerContextReducer,
initial() initial
); );
useEffect(() => { useEffect(() => {
dispatch({ dispatch({
type: "UPDATE_PLAYER", type: "UPDATE_PLAYER",
player: props.player.current, state: playerState,
playerWrapper: props.playerWrapper.current,
}); });
}, [props.player, props.playerWrapper]); }, [playerState]);
return ( return (
<VideoPlayerContext.Provider value={videoData}> <VideoPlayerContext.Provider value={videoData}>
@ -101,3 +75,11 @@ export function VideoPlayerContextProvider(props: {
</VideoPlayerContext.Provider> </VideoPlayerContext.Provider>
); );
} }
export function useVideoPlayerState() {
const { state } = useContext(VideoPlayerContext);
return {
videoState: state,
};
}

View file

@ -1,37 +1,15 @@
import { forwardRef, useCallback, useContext, useEffect, useRef } from "react"; import { forwardRef, useContext, useRef } from "react";
import { import { VideoPlayerContext, VideoPlayerContextProvider } from "./VideoContext";
VideoPlayerContext,
VideoPlayerContextProvider,
VideoPlayerDispatchContext,
} from "./VideoContext";
interface VideoPlayerProps { interface VideoPlayerProps {
children?: React.ReactNode; children?: React.ReactNode;
} }
const VideoPlayerInternals = forwardRef<HTMLVideoElement>((props, ref) => { const VideoPlayerInternals = forwardRef<HTMLVideoElement>((_, ref) => {
const video = useContext(VideoPlayerContext); 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 ( return (
<video ref={ref} onPlay={onPlay} onPause={onPause} controls> <video controls ref={ref}>
{video.source ? <source src={video.source} type="video/mp4" /> : null} {video.source ? <source src={video.source} type="video/mp4" /> : null}
</video> </video>
); );
@ -39,17 +17,11 @@ const VideoPlayerInternals = forwardRef<HTMLVideoElement>((props, ref) => {
export function VideoPlayer(props: VideoPlayerProps) { export function VideoPlayer(props: VideoPlayerProps) {
const playerRef = useRef<HTMLVideoElement | null>(null); const playerRef = useRef<HTMLVideoElement | null>(null);
const playerWrapperRef = useRef<HTMLDivElement | null>(null);
return ( return (
<VideoPlayerContextProvider <VideoPlayerContextProvider player={playerRef}>
player={playerRef} <VideoPlayerInternals ref={playerRef} />
playerWrapper={playerWrapperRef} {props.children}
>
<div ref={playerWrapperRef} className="bg-blue-900">
<VideoPlayerInternals ref={playerRef} />
{props.children}
</div>
</VideoPlayerContextProvider> </VideoPlayerContextProvider>
); );
} }

View file

@ -1,26 +1,27 @@
import { useCallback, useContext } from "react"; // import { useCallback, useContext } from "react";
import { // import {
VideoPlayerContext, // VideoPlayerContext,
VideoPlayerDispatchContext, // VideoPlayerDispatchContext,
} from "../VideoContext"; // } from "../VideoContext";
export function FullscreenControl() { export function FullscreenControl() {
const dispatch = useContext(VideoPlayerDispatchContext); return <p>Hello world</p>;
const video = useContext(VideoPlayerContext); // const dispatch = useContext(VideoPlayerDispatchContext);
// const video = useContext(VideoPlayerContext);
const handleClick = useCallback(() => { // const handleClick = useCallback(() => {
dispatch({ // dispatch({
type: "FULLSCREEN", // type: "FULLSCREEN",
do: video.fullscreen ? "EXIT" : "ENTER", // do: video.fullscreen ? "EXIT" : "ENTER",
}); // });
}, [video, dispatch]); // }, [video, dispatch]);
let text = "not fullscreen"; // let text = "not fullscreen";
if (video.fullscreen) text = "in fullscreen"; // if (video.fullscreen) text = "in fullscreen";
return ( // return (
<button type="button" onClick={handleClick}> // <button type="button" onClick={handleClick}>
{text} // {text}
</button> // </button>
); // );
} }

View file

@ -1,28 +1,16 @@
import { useCallback, useContext } from "react"; import { useCallback } from "react";
import { import { useVideoPlayerState } from "../VideoContext";
VideoPlayerContext,
VideoPlayerDispatchContext,
} from "../VideoContext";
export function PauseControl() { export function PauseControl() {
const dispatch = useContext(VideoPlayerDispatchContext); const { videoState } = useVideoPlayerState();
const video = useContext(VideoPlayerContext);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (video.controlState === "playing") if (videoState?.isPlaying) videoState.pause();
dispatch({ else videoState.play();
type: "CONTROL", }, [videoState]);
do: "PAUSE",
});
else if (video.controlState === "paused")
dispatch({
type: "CONTROL",
do: "PLAY",
});
}, [video, dispatch]);
let text = "paused"; let text = "paused";
if (video.controlState === "playing") text = "playing"; if (videoState?.isPlaying) text = "playing";
return ( return (
<button type="button" onClick={handleClick}> <button type="button" onClick={handleClick}>

View file

@ -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();
},
};
}

View file

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