mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 04:31:51 +01:00
custom video player start
This commit is contained in:
parent
b98fdcd94d
commit
eeaa4d7571
103
src/components/video/VideoContext.tsx
Normal file
103
src/components/video/VideoContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
55
src/components/video/VideoPlayer.tsx
Normal file
55
src/components/video/VideoPlayer.tsx
Normal 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>
|
||||
);
|
||||
}
|
26
src/components/video/controls/FullscreenControl.tsx
Normal file
26
src/components/video/controls/FullscreenControl.tsx
Normal 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>
|
||||
);
|
||||
}
|
32
src/components/video/controls/PauseControl.tsx
Normal file
32
src/components/video/controls/PauseControl.tsx
Normal 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>
|
||||
);
|
||||
}
|
19
src/components/video/controls/SourceControl.tsx
Normal file
19
src/components/video/controls/SourceControl.tsx
Normal 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;
|
||||
}
|
@ -6,6 +6,7 @@ import { WatchedContextProvider } from "@/state/watched";
|
||||
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||
import { MediaView } from "@/views/MediaView";
|
||||
import { SearchView } from "@/views/search/SearchView";
|
||||
import { TestView } from "@/views/TestView";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -18,6 +19,7 @@ function App() {
|
||||
<Route exact path="/media/movie/:media" component={MediaView} />
|
||||
<Route exact path="/media/series/:media" component={MediaView} />
|
||||
<Route exact path="/search/:type/:query?" component={SearchView} />
|
||||
<Route exact path="/test" component={TestView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</BookmarkContextProvider>
|
||||
|
16
src/views/TestView.tsx
Normal file
16
src/views/TestView.tsx
Normal 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>
|
||||
);
|
||||
}
|
33
yarn.lock
33
yarn.lock
@ -10,7 +10,7 @@
|
||||
"core-js-pure" "^3.25.1"
|
||||
"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=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz"
|
||||
"version" "7.20.6"
|
||||
@ -780,7 +780,7 @@
|
||||
dependencies:
|
||||
"ip-regex" "^4.1.0"
|
||||
|
||||
"classnames@^2.0.0":
|
||||
"classnames@^2.0.0", "classnames@^2.2.6":
|
||||
"integrity" "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||
"resolved" "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
|
||||
"version" "2.3.2"
|
||||
@ -2101,6 +2101,11 @@
|
||||
"resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||
"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":
|
||||
"integrity" "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"resolved" "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
@ -2787,7 +2792,7 @@
|
||||
dependencies:
|
||||
"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=="
|
||||
"resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
"version" "15.8.1"
|
||||
@ -2821,7 +2826,7 @@
|
||||
dependencies:
|
||||
"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=="
|
||||
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
|
||||
"version" "17.0.2"
|
||||
@ -2882,7 +2887,7 @@
|
||||
"shallowequal" "^1.0.0"
|
||||
"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=="
|
||||
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
||||
"version" "17.0.2"
|
||||
@ -2941,6 +2946,13 @@
|
||||
dependencies:
|
||||
"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":
|
||||
"integrity" "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"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"
|
||||
"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":
|
||||
"integrity" "sha512-xCJMR0KD4rqSUwINyHJlLizio2VzYzaMrRkqC9xWaVGXgw1lIrzdD+wBUf1XDM8EhL1JoQ7aykLOfKrlZd1SoQ=="
|
||||
"resolved" "https://registry.npmjs.org/vite-plugin-package-version/-/vite-plugin-package-version-1.0.2.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user