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

View file

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

View file

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

View file

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

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