diff --git a/src/components/video/VideoContext.tsx b/src/components/video/VideoContext.tsx new file mode 100644 index 00000000..dd8301d9 --- /dev/null +++ b/src/components/video/VideoContext.tsx @@ -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( + initial() +); +export const VideoPlayerDispatchContext = createContext< + React.Dispatch +>(null as any); + +export function VideoPlayerContextProvider(props: { + children: React.ReactNode; + player: MutableRefObject; + playerWrapper: MutableRefObject; +}) { + const [videoData, dispatch] = useReducer( + videoPlayerContextReducer, + initial() + ); + + useEffect(() => { + dispatch({ + type: "UPDATE_PLAYER", + player: props.player.current, + playerWrapper: props.playerWrapper.current, + }); + }, [props.player, props.playerWrapper]); + + return ( + + + {props.children} + + + ); +} diff --git a/src/components/video/VideoPlayer.tsx b/src/components/video/VideoPlayer.tsx new file mode 100644 index 00000000..d2395bb7 --- /dev/null +++ b/src/components/video/VideoPlayer.tsx @@ -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((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 ( + + ); +}); + +export function VideoPlayer(props: VideoPlayerProps) { + const playerRef = useRef(null); + const playerWrapperRef = useRef(null); + + return ( + +
+ + {props.children} +
+
+ ); +} diff --git a/src/components/video/controls/FullscreenControl.tsx b/src/components/video/controls/FullscreenControl.tsx new file mode 100644 index 00000000..332df675 --- /dev/null +++ b/src/components/video/controls/FullscreenControl.tsx @@ -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 ( + + ); +} diff --git a/src/components/video/controls/PauseControl.tsx b/src/components/video/controls/PauseControl.tsx new file mode 100644 index 00000000..9ea9bcb6 --- /dev/null +++ b/src/components/video/controls/PauseControl.tsx @@ -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 ( + + ); +} diff --git a/src/components/video/controls/SourceControl.tsx b/src/components/video/controls/SourceControl.tsx new file mode 100644 index 00000000..e7ad0c9d --- /dev/null +++ b/src/components/video/controls/SourceControl.tsx @@ -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; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index b23cf7ee..091aa967 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -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() { + diff --git a/src/views/TestView.tsx b/src/views/TestView.tsx new file mode 100644 index 00000000..01e0af45 --- /dev/null +++ b/src/views/TestView.tsx @@ -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 ( + + + + + + ); +} diff --git a/yarn.lock b/yarn.lock index bd46f9e2..75b2fabb 100644 --- a/yarn.lock +++ b/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"