fullscreen video

This commit is contained in:
Jelle van Snik 2023-01-08 16:23:42 +01:00
parent f93b9b5b0f
commit 218a14d5f6
7 changed files with 84 additions and 40 deletions

View File

@ -53,8 +53,9 @@ 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>;
wrapper: MutableRefObject<HTMLDivElement | null>;
}) { }) {
const { playerState } = useVideoPlayer(props.player); const { playerState } = useVideoPlayer(props.player, props.wrapper);
const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>( const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>(
videoPlayerContextReducer, videoPlayerContextReducer,
initial initial

View File

@ -9,7 +9,7 @@ const VideoPlayerInternals = forwardRef<HTMLVideoElement>((_, ref) => {
const video = useContext(VideoPlayerContext); const video = useContext(VideoPlayerContext);
return ( return (
<video controls ref={ref}> <video ref={ref} className="h-full w-full">
{video.source ? <source src={video.source} type="video/mp4" /> : null} {video.source ? <source src={video.source} type="video/mp4" /> : null}
</video> </video>
); );
@ -17,11 +17,17 @@ const VideoPlayerInternals = forwardRef<HTMLVideoElement>((_, 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 player={playerRef}> <VideoPlayerContextProvider player={playerRef} wrapper={playerWrapperRef}>
<div
className="relative aspect-video w-full bg-black"
ref={playerWrapperRef}
>
<VideoPlayerInternals ref={playerRef} /> <VideoPlayerInternals ref={playerRef} />
{props.children} <div className="absolute inset-0">{props.children}</div>
</div>
</VideoPlayerContextProvider> </VideoPlayerContextProvider>
); );
} }

View File

@ -1,27 +1,24 @@
// import { useCallback, useContext } from "react"; import { useCallback } from "react";
// import { import { useVideoPlayerState } from "../VideoContext";
// VideoPlayerContext,
// VideoPlayerDispatchContext,
// } from "../VideoContext";
export function FullscreenControl() { export function FullscreenControl() {
return <p>Hello world</p>; const { videoState } = useVideoPlayerState();
// const dispatch = useContext(VideoPlayerDispatchContext);
// const video = useContext(VideoPlayerContext);
// const handleClick = useCallback(() => { const handleClick = useCallback(() => {
// dispatch({ if (videoState.isFullscreen) videoState.exitFullscreen();
// type: "FULLSCREEN", else videoState.enterFullscreen();
// do: video.fullscreen ? "EXIT" : "ENTER", }, [videoState]);
// });
// }, [video, dispatch]);
// let text = "not fullscreen"; let text = "not fullscreen";
// if (video.fullscreen) text = "in fullscreen"; if (videoState.isFullscreen) text = "in fullscreen";
// return ( return (
// <button type="button" onClick={handleClick}> <button
// {text} className="m-1 rounded bg-denim-100 p-1 text-white hover:opacity-75"
// </button> type="button"
// ); onClick={handleClick}
>
{text}
</button>
);
} }

View File

@ -9,11 +9,15 @@ export function PauseControl() {
else videoState.play(); else videoState.play();
}, [videoState]); }, [videoState]);
let text = "paused"; let text =
if (videoState?.isPlaying) text = "playing"; videoState.isPlaying || videoState.isSeeking ? "playing" : "paused";
return ( return (
<button type="button" onClick={handleClick}> <button
className="m-1 rounded bg-denim-100 p-1 text-white hover:opacity-75"
type="button"
onClick={handleClick}
>
{text} {text}
</button> </button>
); );

View File

@ -1,14 +1,21 @@
export interface PlayerControls { export interface PlayerControls {
play(): void; play(): void;
pause(): void; pause(): void;
exitFullscreen(): void;
enterFullscreen(): void;
} }
export const initialControls: PlayerControls = { export const initialControls: PlayerControls = {
play: () => null, play: () => null,
pause: () => null, pause: () => null,
enterFullscreen: () => null,
exitFullscreen: () => null,
}; };
export function populateControls(player: HTMLVideoElement): PlayerControls { export function populateControls(
player: HTMLVideoElement,
wrapper: HTMLDivElement
): PlayerControls {
return { return {
play() { play() {
player.play(); player.play();
@ -16,5 +23,13 @@ export function populateControls(player: HTMLVideoElement): PlayerControls {
pause() { pause() {
player.pause(); player.pause();
}, },
enterFullscreen() {
if (!document.fullscreenEnabled || document.fullscreenElement) return;
wrapper.requestFullscreen();
},
exitFullscreen() {
if (!document.fullscreenElement) return;
document.exitFullscreen();
},
}; };
} }

View File

@ -8,11 +8,15 @@ import {
export type PlayerState = { export type PlayerState = {
isPlaying: boolean; isPlaying: boolean;
isPaused: boolean; isPaused: boolean;
isSeeking: boolean;
isFullscreen: boolean;
} & PlayerControls; } & PlayerControls;
export const initialPlayerState = { export const initialPlayerState: PlayerState = {
isPlaying: false, isPlaying: false,
isPaused: true, isPaused: true,
isFullscreen: false,
isSeeking: false,
...initialControls, ...initialControls,
}; };
@ -24,6 +28,8 @@ function readState(player: HTMLVideoElement, update: SetPlayer) {
}; };
state.isPaused = player.paused; state.isPaused = player.paused;
state.isPlaying = !player.paused; state.isPlaying = !player.paused;
state.isFullscreen = !!document.fullscreenElement;
state.isSeeking = player.seeking;
update(state); update(state);
} }
@ -35,19 +41,32 @@ function registerListeners(player: HTMLVideoElement, update: SetPlayer) {
player.addEventListener("play", () => { player.addEventListener("play", () => {
update((s) => ({ ...s, isPaused: false, isPlaying: true })); update((s) => ({ ...s, isPaused: false, isPlaying: true }));
}); });
player.addEventListener("seeking", () => {
update((s) => ({ ...s, isSeeking: true }));
});
player.addEventListener("seeked", () => {
update((s) => ({ ...s, isSeeking: false }));
});
document.addEventListener("fullscreenchange", () => {
update((s) => ({ ...s, isFullscreen: !!document.fullscreenElement }));
});
} }
export function useVideoPlayer(ref: MutableRefObject<HTMLVideoElement | null>) { export function useVideoPlayer(
ref: MutableRefObject<HTMLVideoElement | null>,
wrapperRef: MutableRefObject<HTMLDivElement | null>
) {
const [state, setState] = useState(initialPlayerState); const [state, setState] = useState(initialPlayerState);
useEffect(() => { useEffect(() => {
const player = ref.current; const player = ref.current;
if (player) { const wrapper = wrapperRef.current;
if (player && wrapper) {
readState(player, setState); readState(player, setState);
registerListeners(player, setState); registerListeners(player, setState);
setState((s) => ({ ...s, ...populateControls(player) })); setState((s) => ({ ...s, ...populateControls(player, wrapper) }));
} }
}, [ref]); }, [ref, wrapperRef]);
return { return {
playerState: state, playerState: state,

View File

@ -7,10 +7,12 @@ import { VideoPlayer } from "@/components/video/VideoPlayer";
export function TestView() { export function TestView() {
return ( return (
<div className="w-[40rem] max-w-full">
<VideoPlayer> <VideoPlayer>
<PauseControl /> <PauseControl />
<FullscreenControl /> <FullscreenControl />
<SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" /> <SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />
</VideoPlayer> </VideoPlayer>
</div>
); );
} }