custom video player start

This commit is contained in:
Jelle van Snik 2023-01-08 13:15:32 +01:00
parent b98fdcd94d
commit eeaa4d7571
8 changed files with 281 additions and 5 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import { WatchedContextProvider } from "@/state/watched";
import { NotFoundPage } from "@/views/notfound/NotFoundView"; import { NotFoundPage } from "@/views/notfound/NotFoundView";
import { MediaView } from "@/views/MediaView"; import { MediaView } from "@/views/MediaView";
import { SearchView } from "@/views/search/SearchView"; import { SearchView } from "@/views/search/SearchView";
import { TestView } from "@/views/TestView";
function App() { function App() {
return ( return (
@ -18,6 +19,7 @@ function App() {
<Route exact path="/media/movie/:media" component={MediaView} /> <Route exact path="/media/movie/:media" component={MediaView} />
<Route exact path="/media/series/:media" component={MediaView} /> <Route exact path="/media/series/:media" component={MediaView} />
<Route exact path="/search/:type/:query?" component={SearchView} /> <Route exact path="/search/:type/:query?" component={SearchView} />
<Route exact path="/test" component={TestView} />
<Route path="*" component={NotFoundPage} /> <Route path="*" component={NotFoundPage} />
</Switch> </Switch>
</BookmarkContextProvider> </BookmarkContextProvider>

16
src/views/TestView.tsx Normal file
View file

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

View file

@ -10,7 +10,7 @@
"core-js-pure" "^3.25.1" "core-js-pure" "^3.25.1"
"regenerator-runtime" "^0.13.11" "regenerator-runtime" "^0.13.11"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6": "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.9", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.4.5", "@babel/runtime@^7.9.2":
"integrity" "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==" "integrity" "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA=="
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" "resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz"
"version" "7.20.6" "version" "7.20.6"
@ -780,7 +780,7 @@
dependencies: dependencies:
"ip-regex" "^4.1.0" "ip-regex" "^4.1.0"
"classnames@^2.0.0": "classnames@^2.0.0", "classnames@^2.2.6":
"integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" "integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
"resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" "resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
"version" "2.3.2" "version" "2.3.2"
@ -2101,6 +2101,11 @@
"resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
"version" "4.6.2" "version" "4.6.2"
"lodash.throttle@^4.1.1":
"integrity" "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
"resolved" "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz"
"version" "4.1.1"
"lodash@^4.17.15": "lodash@^4.17.15":
"integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" "resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
@ -2787,7 +2792,7 @@
dependencies: dependencies:
"read" "1" "read" "1"
"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1": "prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1":
"integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==" "integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="
"resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" "resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
"version" "15.8.1" "version" "15.8.1"
@ -2821,7 +2826,7 @@
dependencies: dependencies:
"performance-now" "^2.1.0" "performance-now" "^2.1.0"
"react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2": "react-dom@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^17.0.2":
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
"version" "17.0.2" "version" "17.0.2"
@ -2882,7 +2887,7 @@
"shallowequal" "^1.0.0" "shallowequal" "^1.0.0"
"subscribe-ui-event" "^2.0.6" "subscribe-ui-event" "^2.0.6"
"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2": "react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2" "version" "17.0.2"
@ -2941,6 +2946,13 @@
dependencies: dependencies:
"picomatch" "^2.2.1" "picomatch" "^2.2.1"
"redux@^4.0.1":
"integrity" "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA=="
"resolved" "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz"
"version" "4.2.0"
dependencies:
"@babel/runtime" "^7.9.2"
"regenerator-runtime@^0.13.11": "regenerator-runtime@^0.13.11":
"integrity" "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" "integrity" "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
"resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" "resolved" "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
@ -3415,6 +3427,17 @@
"resolved" "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz" "resolved" "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz"
"version" "1.0.1" "version" "1.0.1"
"video-react@^0.16.0":
"integrity" "sha512-138NHPS8bmgqCYVCdbv2GVFhXntemNHWGw9AN8iJSzr3jizXMmWJd2LTBppr4hZJUbyW1A1tPZ3CQXZUaexMVA=="
"resolved" "https://registry.npmjs.org/video-react/-/video-react-0.16.0.tgz"
"version" "0.16.0"
dependencies:
"@babel/runtime" "^7.4.5"
"classnames" "^2.2.6"
"lodash.throttle" "^4.1.1"
"prop-types" "^15.7.2"
"redux" "^4.0.1"
"vite-plugin-package-version@^1.0.2": "vite-plugin-package-version@^1.0.2":
"integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ==" "integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ=="
"resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz" "resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz"