8 changed files with 281 additions and 5 deletions
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
import React, { |
||||
createContext, |
||||
MutableRefObject, |
||||
useEffect, |
||||
useReducer, |
||||
} from "react"; |
||||
|
||||
interface VideoPlayerContextType { |
||||
source: null | string; |
||||
playerWrapper: HTMLDivElement | null; |
||||
player: HTMLVideoElement | null; |
||||
controlState: "paused" | "playing"; |
||||
fullscreen: boolean; |
||||
} |
||||
const initial = ( |
||||
player: HTMLVideoElement | null = null, |
||||
wrapper: HTMLDivElement | null = null |
||||
): VideoPlayerContextType => ({ |
||||
source: null, |
||||
playerWrapper: wrapper, |
||||
player, |
||||
controlState: "paused", |
||||
fullscreen: false, |
||||
}); |
||||
|
||||
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; |
||||
}; |
||||
|
||||
function videoPlayerContextReducer( |
||||
original: VideoPlayerContextType, |
||||
action: VideoPlayerContextAction |
||||
): VideoPlayerContextType { |
||||
const video = { ...original }; |
||||
if (action.type === "SET_SOURCE") { |
||||
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(); |
||||
return video; |
||||
} |
||||
|
||||
return original; |
||||
} |
||||
|
||||
export const VideoPlayerContext = createContext<VideoPlayerContextType>( |
||||
initial() |
||||
); |
||||
export const VideoPlayerDispatchContext = createContext< |
||||
React.Dispatch<VideoPlayerContextAction> |
||||
>(null as any); |
||||
|
||||
export function VideoPlayerContextProvider(props: { |
||||
children: React.ReactNode; |
||||
player: MutableRefObject<HTMLVideoElement | null>; |
||||
playerWrapper: MutableRefObject<HTMLDivElement | null>; |
||||
}) { |
||||
const [videoData, dispatch] = useReducer<typeof videoPlayerContextReducer>( |
||||
videoPlayerContextReducer, |
||||
initial() |
||||
); |
||||
|
||||
useEffect(() => { |
||||
dispatch({ |
||||
type: "UPDATE_PLAYER", |
||||
player: props.player.current, |
||||
playerWrapper: props.playerWrapper.current, |
||||
}); |
||||
}, [props.player, props.playerWrapper]); |
||||
|
||||
return ( |
||||
<VideoPlayerContext.Provider value={videoData}> |
||||
<VideoPlayerDispatchContext.Provider value={dispatch}> |
||||
{props.children} |
||||
</VideoPlayerDispatchContext.Provider> |
||||
</VideoPlayerContext.Provider> |
||||
); |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
import { forwardRef, useCallback, useContext, useEffect, useRef } from "react"; |
||||
import { |
||||
VideoPlayerContext, |
||||
VideoPlayerContextProvider, |
||||
VideoPlayerDispatchContext, |
||||
} from "./VideoContext"; |
||||
|
||||
interface VideoPlayerProps { |
||||
children?: React.ReactNode; |
||||
} |
||||
|
||||
const VideoPlayerInternals = forwardRef<HTMLVideoElement>((props, 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.source ? <source src={video.source} type="video/mp4" /> : null} |
||||
</video> |
||||
); |
||||
}); |
||||
|
||||
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> |
||||
); |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
import { useCallback, useContext } from "react"; |
||||
import { |
||||
VideoPlayerContext, |
||||
VideoPlayerDispatchContext, |
||||
} from "../VideoContext"; |
||||
|
||||
export function FullscreenControl() { |
||||
const dispatch = useContext(VideoPlayerDispatchContext); |
||||
const video = useContext(VideoPlayerContext); |
||||
|
||||
const handleClick = useCallback(() => { |
||||
dispatch({ |
||||
type: "FULLSCREEN", |
||||
do: video.fullscreen ? "EXIT" : "ENTER", |
||||
}); |
||||
}, [video, dispatch]); |
||||
|
||||
let text = "not fullscreen"; |
||||
if (video.fullscreen) text = "in fullscreen"; |
||||
|
||||
return ( |
||||
<button type="button" onClick={handleClick}> |
||||
{text} |
||||
</button> |
||||
); |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useContext } from "react"; |
||||
import { |
||||
VideoPlayerContext, |
||||
VideoPlayerDispatchContext, |
||||
} from "../VideoContext"; |
||||
|
||||
export function PauseControl() { |
||||
const dispatch = useContext(VideoPlayerDispatchContext); |
||||
const video = useContext(VideoPlayerContext); |
||||
|
||||
const handleClick = useCallback(() => { |
||||
if (video.controlState === "playing") |
||||
dispatch({ |
||||
type: "CONTROL", |
||||
do: "PAUSE", |
||||
}); |
||||
else if (video.controlState === "paused") |
||||
dispatch({ |
||||
type: "CONTROL", |
||||
do: "PLAY", |
||||
}); |
||||
}, [video, dispatch]); |
||||
|
||||
let text = "paused"; |
||||
if (video.controlState === "playing") text = "playing"; |
||||
|
||||
return ( |
||||
<button type="button" onClick={handleClick}> |
||||
{text} |
||||
</button> |
||||
); |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
import { useContext, useEffect } from "react"; |
||||
import { VideoPlayerDispatchContext } from "../VideoContext"; |
||||
|
||||
interface SourceControlProps { |
||||
source: string; |
||||
} |
||||
|
||||
export function SourceControl(props: SourceControlProps) { |
||||
const dispatch = useContext(VideoPlayerDispatchContext); |
||||
|
||||
useEffect(() => { |
||||
dispatch({ |
||||
type: "SET_SOURCE", |
||||
url: props.source, |
||||
}); |
||||
}, [props.source, dispatch]); |
||||
|
||||
return null; |
||||
} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
import { FullscreenControl } from "@/components/video/controls/FullscreenControl"; |
||||
import { PauseControl } from "@/components/video/controls/PauseControl"; |
||||
import { SourceControl } from "@/components/video/controls/SourceControl"; |
||||
import { VideoPlayer } from "@/components/video/VideoPlayer"; |
||||
|
||||
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
|
||||
|
||||
export function TestView() { |
||||
return ( |
||||
<VideoPlayer> |
||||
<PauseControl /> |
||||
<FullscreenControl /> |
||||
<SourceControl source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" /> |
||||
</VideoPlayer> |
||||
); |
||||
} |
Loading…
Reference in new issue