mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 18:51:53 +01:00
add basic functioning player
This commit is contained in:
parent
6ca3196b75
commit
c5a8065db9
@ -1,12 +1,12 @@
|
|||||||
import { useGoBack } from "@/hooks/useGoBack";
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
||||||
import { forwardRef, useContext, useEffect, useRef } from "react";
|
import { forwardRef, useContext, useEffect, useRef } from "react";
|
||||||
import { VideoErrorBoundary } from "../../components/video/parts/VideoErrorBoundary";
|
import { VideoErrorBoundary } from "./parts/VideoErrorBoundary";
|
||||||
import {
|
import {
|
||||||
useVideoPlayerState,
|
useVideoPlayerState,
|
||||||
VideoPlayerContext,
|
VideoPlayerContext,
|
||||||
VideoPlayerContextProvider,
|
VideoPlayerContextProvider,
|
||||||
} from "../../video/components./../components/video/VideoContext";
|
} from "./VideoContext";
|
||||||
|
|
||||||
export interface VideoPlayerProps {
|
export interface VideoPlayerProps {
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
@ -1,4 +1,4 @@
|
|||||||
import { useVideoPlayerState } from "@/components/video/VideoContext";
|
import { useVideoPlayerState } from "@/../__old/VideoContext";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function useVolumeControl() {
|
export function useVolumeControl() {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { VideoPlayerContextProvider } from "../state/hooks";
|
import { VideoPlayerContextProvider } from "../state/hooks";
|
||||||
|
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
||||||
|
|
||||||
export interface VideoPlayerProps {
|
export interface VideoPlayerBaseProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayer(props: VideoPlayerProps) {
|
export function VideoPlayerBase(props: VideoPlayerBaseProps) {
|
||||||
// TODO error boundary
|
// TODO error boundary
|
||||||
// TODO move error boundary to only decorated, <VideoPlayer /> shouldn't have styling
|
// TODO move error boundary to only decorated, <VideoPlayer /> shouldn't have styling
|
||||||
// TODO internal controls
|
// TODO internal controls
|
||||||
@ -12,6 +13,7 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
|||||||
return (
|
return (
|
||||||
<VideoPlayerContextProvider>
|
<VideoPlayerContextProvider>
|
||||||
<div className="is-video-player relative h-full w-full select-none overflow-hidden bg-black [border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]">
|
<div className="is-video-player relative h-full w-full select-none overflow-hidden bg-black [border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]">
|
||||||
|
<VideoElementInternal />
|
||||||
<div className="absolute inset-0">{props.children}</div>
|
<div className="absolute inset-0">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</VideoPlayerContextProvider>
|
</VideoPlayerContextProvider>
|
||||||
|
34
src/video/components/actions/PauseAction.tsx
Normal file
34
src/video/components/actions/PauseAction.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
iconSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PauseAction(props: Props) {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (mediaPlaying.isPlaying) controls.pause();
|
||||||
|
else controls.play();
|
||||||
|
}, [mediaPlaying, controls]);
|
||||||
|
|
||||||
|
// TODO add seeking back
|
||||||
|
const icon = mediaPlaying.isPlaying ? Icons.PAUSE : Icons.PLAY;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerIconButton
|
||||||
|
iconSize={props.iconSize}
|
||||||
|
className={props.className}
|
||||||
|
icon={icon}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
30
src/video/components/internal/VideoElementInternal.tsx
Normal file
30
src/video/components/internal/VideoElementInternal.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
||||||
|
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function VideoElementInternal() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const ref = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const provider = createVideoStateProvider(descriptor, ref.current);
|
||||||
|
setProvider(descriptor, provider);
|
||||||
|
const { destroy } = provider.providerStart();
|
||||||
|
return () => {
|
||||||
|
unsetStateProvider(descriptor);
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
// TODO autoplay and muted
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
ref={ref}
|
||||||
|
playsInline
|
||||||
|
className="h-full w-full"
|
||||||
|
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
27
src/video/components/parts/VideoPlayerIconButton.tsx
Normal file
27
src/video/components/parts/VideoPlayerIconButton.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface VideoPlayerIconButtonProps {
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
icon: Icons;
|
||||||
|
text?: string;
|
||||||
|
className?: string;
|
||||||
|
iconSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoPlayerIconButton(props: VideoPlayerIconButtonProps) {
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={props.onClick}
|
||||||
|
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center rounded-full bg-white bg-opacity-0 p-2 transition-colors duration-100 group-hover:bg-opacity-20">
|
||||||
|
<Icon icon={props.icon} className={props.iconSize ?? "text-2xl"} />
|
||||||
|
{props.text ? <span className="ml-2">{props.text}</span> : null}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
export type VideoPlayerEvent = "progress";
|
export type VideoPlayerEvent = "mediaplaying";
|
||||||
|
|
||||||
function createEventString(id: string, event: VideoPlayerEvent): string {
|
function createEventString(id: string, event: VideoPlayerEvent): string {
|
||||||
return `_vid:::${id}:::${event}`;
|
return `_vid:::${id}:::${event}`;
|
||||||
|
15
src/video/state/logic/controls.ts
Normal file
15
src/video/state/logic/controls.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { VideoPlayerStateController } from "../providers/providerTypes";
|
||||||
|
|
||||||
|
export function useControls(descriptor: string): VideoPlayerStateController {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pause() {
|
||||||
|
state.stateProvider?.pause();
|
||||||
|
},
|
||||||
|
play() {
|
||||||
|
state.stateProvider?.play();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
52
src/video/state/logic/mediaplaying.ts
Normal file
52
src/video/state/logic/mediaplaying.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||||
|
import { VideoPlayerState } from "../types";
|
||||||
|
|
||||||
|
export type VideoMediaPlayingEvent = {
|
||||||
|
isPlaying: boolean;
|
||||||
|
isPaused: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
hasPlayedOnce: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMediaPlayingFromState(
|
||||||
|
state: VideoPlayerState
|
||||||
|
): VideoMediaPlayingEvent {
|
||||||
|
return {
|
||||||
|
hasPlayedOnce: state.hasPlayedOnce,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
isPaused: state.isPaused,
|
||||||
|
isPlaying: state.isPlaying,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMediaPlaying(
|
||||||
|
descriptor: string,
|
||||||
|
state: VideoPlayerState
|
||||||
|
) {
|
||||||
|
sendEvent<VideoMediaPlayingEvent>(
|
||||||
|
descriptor,
|
||||||
|
"mediaplaying",
|
||||||
|
getMediaPlayingFromState(state)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMediaPlaying(descriptor: string): VideoMediaPlayingEvent {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const [data, setData] = useState<VideoMediaPlayingEvent>(
|
||||||
|
getMediaPlayingFromState(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(payload: CustomEvent<VideoMediaPlayingEvent>) {
|
||||||
|
setData(payload.detail);
|
||||||
|
}
|
||||||
|
listenEvent(descriptor, "mediaplaying", update);
|
||||||
|
return () => {
|
||||||
|
unlistenEvent(descriptor, "mediaplaying", update);
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
export type VideoPlayerStateProvider = {
|
export type VideoPlayerStateController = {
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
play: () => void;
|
play: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
||||||
providerStart: () => {
|
providerStart: () => {
|
||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
};
|
};
|
||||||
|
18
src/video/state/providers/utils.ts
Normal file
18
src/video/state/providers/utils.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
|
|
||||||
|
export function setProvider(
|
||||||
|
descriptor: string,
|
||||||
|
provider: VideoPlayerStateProvider
|
||||||
|
) {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
state.stateProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: This only sets the state provider to null. it does not destroy the listener
|
||||||
|
*/
|
||||||
|
export function unsetStateProvider(descriptor: string) {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
state.stateProvider = null;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
|
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
|
|
||||||
export function createVideoStateProvider(
|
export function createVideoStateProvider(
|
||||||
@ -15,16 +16,17 @@ export function createVideoStateProvider(
|
|||||||
player.pause();
|
player.pause();
|
||||||
},
|
},
|
||||||
providerStart() {
|
providerStart() {
|
||||||
// TODO reactivity through events
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
state.isPaused = true;
|
state.isPaused = true;
|
||||||
state.isPlaying = false;
|
state.isPlaying = false;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
};
|
};
|
||||||
const playing = () => {
|
const playing = () => {
|
||||||
state.isPaused = false;
|
state.isPaused = false;
|
||||||
state.isPlaying = true;
|
state.isPlaying = true;
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.hasPlayedOnce = true;
|
state.hasPlayedOnce = true;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
};
|
};
|
||||||
|
|
||||||
player.addEventListener("pause", pause);
|
player.addEventListener("pause", pause);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
||||||
|
|
||||||
export type VideoPlayerState = {
|
export type VideoPlayerState = {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
@ -33,4 +35,5 @@ export type VideoPlayerState = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
canAirplay: boolean;
|
canAirplay: boolean;
|
||||||
|
stateProvider: VideoPlayerStateProvider | null;
|
||||||
};
|
};
|
||||||
|
@ -1,35 +1,30 @@
|
|||||||
import {
|
// import {
|
||||||
useChromecast,
|
// useChromecast,
|
||||||
useChromecastAvailable,
|
// useChromecastAvailable,
|
||||||
} from "@/hooks/useChromecastAvailable";
|
// } from "@/hooks/useChromecastAvailable";
|
||||||
import { useEffect, useRef } from "react";
|
// import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
function ChromeCastButton() {
|
import { PauseAction } from "@/video/components/actions/PauseAction";
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
|
||||||
const available = useChromecastAvailable();
|
|
||||||
|
|
||||||
useEffect(() => {
|
// function ChromeCastButton() {
|
||||||
if (!available) return;
|
// const ref = useRef<HTMLDivElement>(null);
|
||||||
const tag = document.createElement("google-cast-launcher");
|
// const available = useChromecastAvailable();
|
||||||
tag.setAttribute("id", "castbutton");
|
|
||||||
ref.current?.appendChild(tag);
|
|
||||||
}, [available]);
|
|
||||||
|
|
||||||
return <div ref={ref} />;
|
// useEffect(() => {
|
||||||
}
|
// if (!available) return;
|
||||||
|
// const tag = document.createElement("google-cast-launcher");
|
||||||
|
// tag.setAttribute("id", "castbutton");
|
||||||
|
// ref.current?.appendChild(tag);
|
||||||
|
// }, [available]);
|
||||||
|
|
||||||
|
// return <div ref={ref} />;
|
||||||
|
// }
|
||||||
|
|
||||||
export function TestView() {
|
export function TestView() {
|
||||||
const { startCast, stopCast } = useChromecast();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<VideoPlayerBase>
|
||||||
<ChromeCastButton />
|
<PauseAction />
|
||||||
<button type="button" onClick={startCast}>
|
</VideoPlayerBase>
|
||||||
Start casting
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={stopCast}>
|
|
||||||
StopCasting
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/../__old/parts/VideoPlayerHeader";
|
||||||
import { useGoBack } from "@/hooks/useGoBack";
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { DecoratedVideoPlayer } from "@/video/components/__old/DecoratedVideoPlayer";
|
import { DecoratedVideoPlayer } from "@/../__old/DecoratedVideoPlayer";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/../__old/parts/VideoPlayerHeader";
|
||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
import { decodeJWId } from "@/backend/metadata/justwatch";
|
||||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
import { SourceControl } from "@/../__old/controls/SourceControl";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
@ -15,8 +15,8 @@ import { useGoBack } from "@/hooks/useGoBack";
|
|||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { useWatchedItem } from "@/state/watched";
|
import { useWatchedItem } from "@/state/watched";
|
||||||
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
import { ProgressListenerControl } from "@/../__old/controls/ProgressListenerControl";
|
||||||
import { ShowControl } from "@/components/video/controls/ShowControl";
|
import { ShowControl } from "@/../__old/controls/ShowControl";
|
||||||
import { MediaFetchErrorView } from "./MediaErrorView";
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||||
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
||||||
|
@ -6,7 +6,7 @@ import { Navigation } from "@/components/layout/Navigation";
|
|||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { useGoBack } from "@/hooks/useGoBack";
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/../__old/parts/VideoPlayerHeader";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
export function NotFoundWrapper(props: {
|
export function NotFoundWrapper(props: {
|
||||||
|
Loading…
Reference in New Issue
Block a user