mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 18:16:57 +00:00
video player starter
This commit is contained in:
parent
eeaa4d7571
commit
3a67d50f42
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
20
src/components/video/hooks/controlVideo.ts
Normal file
20
src/components/video/hooks/controlVideo.ts
Normal 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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
55
src/components/video/hooks/useVideoPlayer.ts
Normal file
55
src/components/video/hooks/useVideoPlayer.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue