add basic functioning player

This commit is contained in:
Jelle van Snik 2023-02-03 15:20:26 +01:00
parent 6ca3196b75
commit c5a8065db9
49 changed files with 223 additions and 42 deletions

View File

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

View File

@ -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() {

View File

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

View 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}
/>
);
}

View 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"
/>
);
}

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

View File

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

View 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();
},
};
}

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {