mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-21 14:47:41 +01:00
meta data in video player
This commit is contained in:
parent
27ef9be6b1
commit
bb14d63a9c
20 changed files with 492 additions and 168 deletions
|
@ -2,13 +2,18 @@ import { Transition } from "@/components/Transition";
|
|||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { BackdropAction } from "@/video/components/actions/BackdropAction";
|
||||
import { FullscreenAction } from "@/video/components/actions/FullscreenAction";
|
||||
import { HeaderAction } from "@/video/components/actions/HeaderAction";
|
||||
import { LoadingAction } from "@/video/components/actions/LoadingAction";
|
||||
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
|
||||
import { MobileCenterAction } from "@/video/components/actions/MobileCenterAction";
|
||||
import { PageTitleAction } from "@/video/components/actions/PageTitleAction";
|
||||
import { PauseAction } from "@/video/components/actions/PauseAction";
|
||||
import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
||||
import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction";
|
||||
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
|
||||
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
|
||||
import { TimeAction } from "@/video/components/actions/TimeAction";
|
||||
import { VideoPlayerError } from "@/video/components/parts/VideoPlayerError";
|
||||
import {
|
||||
VideoPlayerBase,
|
||||
VideoPlayerBaseProps,
|
||||
|
@ -17,6 +22,10 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|||
import { useControls } from "@/video/state/logic/controls";
|
||||
import { ReactNode, useCallback, useState } from "react";
|
||||
|
||||
type Props = VideoPlayerBaseProps & {
|
||||
onGoBack?: () => void;
|
||||
};
|
||||
|
||||
function CenterPosition(props: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
|
@ -48,12 +57,12 @@ function LeftSideControls() {
|
|||
{/* <VolumeControl className="mr-2" /> */}
|
||||
<TimeAction />
|
||||
</div>
|
||||
{/* <ShowTitleControl /> */}
|
||||
<ShowTitleAction />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function VideoPlayer(props: VideoPlayerBaseProps) {
|
||||
export function VideoPlayer(props: Props) {
|
||||
const [show, setShow] = useState(false);
|
||||
const { isMobile } = useIsMobile();
|
||||
|
||||
|
@ -65,76 +74,72 @@ export function VideoPlayer(props: VideoPlayerBaseProps) {
|
|||
);
|
||||
|
||||
// TODO autoplay
|
||||
// TODO meta data
|
||||
// TODO safe area only if full screen or fill screen
|
||||
return (
|
||||
<VideoPlayerBase>
|
||||
{/* <PageTitleControl media={props.media?.meta} /> */}
|
||||
{/* <VideoPlayerError media={props.media?.meta} onGoBack={props.onGoBack}> */}
|
||||
<BackdropAction onBackdropChange={onBackdropChange}>
|
||||
<CenterPosition>
|
||||
<LoadingAction />
|
||||
</CenterPosition>
|
||||
<CenterPosition>
|
||||
<MiddlePauseAction />
|
||||
</CenterPosition>
|
||||
{isMobile ? (
|
||||
<PageTitleAction />
|
||||
<VideoPlayerError onGoBack={props.onGoBack}>
|
||||
<BackdropAction onBackdropChange={onBackdropChange}>
|
||||
<CenterPosition>
|
||||
<LoadingAction />
|
||||
</CenterPosition>
|
||||
<CenterPosition>
|
||||
<MiddlePauseAction />
|
||||
</CenterPosition>
|
||||
{isMobile ? (
|
||||
<Transition
|
||||
animation="fade"
|
||||
show={show}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<MobileCenterAction />
|
||||
</Transition>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Transition
|
||||
animation="fade"
|
||||
animation="slide-down"
|
||||
show={show}
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"
|
||||
>
|
||||
<MobileCenterAction />
|
||||
<HeaderAction showControls={isMobile} onClick={props.onGoBack} />
|
||||
</Transition>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Transition
|
||||
animation="slide-down"
|
||||
show={show}
|
||||
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"
|
||||
>
|
||||
{/* <VideoPlayerHeader
|
||||
media={props.media?.meta}
|
||||
onClick={props.onGoBack}
|
||||
isMobile={isMobile}
|
||||
/> */}
|
||||
</Transition>
|
||||
<Transition
|
||||
animation="slide-up"
|
||||
show={show}
|
||||
className="pointer-events-auto absolute inset-x-0 bottom-0 flex flex-col px-4 pb-2 [margin-bottom:env(safe-area-inset-bottom)]"
|
||||
>
|
||||
<div className="flex w-full items-center space-x-3">
|
||||
{isMobile && <TimeAction noDuration />}
|
||||
<ProgressAction />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isMobile ? (
|
||||
<div className="grid w-full grid-cols-[56px,1fr,56px] items-center">
|
||||
<div />
|
||||
<div className="flex items-center justify-center">
|
||||
{/* <SeriesSelectionControl /> */}
|
||||
{/* <SourceSelectionControl media={props.media} /> */}
|
||||
<Transition
|
||||
animation="slide-up"
|
||||
show={show}
|
||||
className="pointer-events-auto absolute inset-x-0 bottom-0 flex flex-col px-4 pb-2 [margin-bottom:env(safe-area-inset-bottom)]"
|
||||
>
|
||||
<div className="flex w-full items-center space-x-3">
|
||||
{isMobile && <TimeAction noDuration />}
|
||||
<ProgressAction />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isMobile ? (
|
||||
<div className="grid w-full grid-cols-[56px,1fr,56px] items-center">
|
||||
<div />
|
||||
<div className="flex items-center justify-center">
|
||||
{/* <SeriesSelectionControl /> */}
|
||||
{/* <SourceSelectionControl media={props.media} /> */}
|
||||
</div>
|
||||
<FullscreenAction />
|
||||
</div>
|
||||
<FullscreenAction />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<LeftSideControls />
|
||||
<div className="flex-1" />
|
||||
{/* <QualityDisplayControl />
|
||||
<SeriesSelectionControl />
|
||||
) : (
|
||||
<>
|
||||
<LeftSideControls />
|
||||
<div className="flex-1" />
|
||||
<QualityDisplayAction />
|
||||
{/* <SeriesSelectionControl />
|
||||
<SourceSelectionControl media={props.media} />
|
||||
<AirplayControl />
|
||||
<ChromeCastControl /> */}
|
||||
<FullscreenAction />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</BackdropAction>
|
||||
{props.children}
|
||||
{/* </VideoPlayerError> */}
|
||||
<FullscreenAction />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</BackdropAction>
|
||||
{props.children}
|
||||
</VideoPlayerError>
|
||||
</VideoPlayerBase>
|
||||
);
|
||||
}
|
||||
|
|
15
src/video/components/actions/HeaderAction.tsx
Normal file
15
src/video/components/actions/HeaderAction.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useMeta } from "@/video/state/logic/meta";
|
||||
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export function HeaderAction(props: Props) {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const meta = useMeta(descriptor);
|
||||
|
||||
return <VideoPlayerHeader media={meta?.meta} {...props} />;
|
||||
}
|
19
src/video/components/actions/PageTitleAction.tsx
Normal file
19
src/video/components/actions/PageTitleAction.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
||||
|
||||
export function PageTitleAction() {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const { isSeries, humanizedEpisodeId, meta } =
|
||||
useCurrentSeriesEpisodeInfo(descriptor);
|
||||
|
||||
if (!meta) return null;
|
||||
|
||||
const title = isSeries ? `${meta.title} - ${humanizedEpisodeId}` : meta.title;
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
17
src/video/components/actions/QualityDisplayAction.tsx
Normal file
17
src/video/components/actions/QualityDisplayAction.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useSource } from "@/video/state/logic/source";
|
||||
|
||||
export function QualityDisplayAction() {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const source = useSource(descriptor);
|
||||
|
||||
if (!source.source) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-md bg-denim-300 py-1 px-2 transition-colors">
|
||||
<p className="text-center text-xs font-bold text-slate-300 transition-colors">
|
||||
{source.source.quality}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
17
src/video/components/actions/ShowTitleAction.tsx
Normal file
17
src/video/components/actions/ShowTitleAction.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useCurrentSeriesEpisodeInfo } from "../hooks/useCurrentSeriesEpisodeInfo";
|
||||
|
||||
export function ShowTitleAction() {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const { isSeries, currentEpisodeInfo, humanizedEpisodeId } =
|
||||
useCurrentSeriesEpisodeInfo(descriptor);
|
||||
|
||||
if (!isSeries) return null;
|
||||
|
||||
return (
|
||||
<p className="ml-8 select-none space-x-2 text-white">
|
||||
<span>{humanizedEpisodeId}</span>
|
||||
<span className="opacity-50">{currentEpisodeInfo?.title}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
19
src/video/components/controllers/MetaController.tsx
Normal file
19
src/video/components/controllers/MetaController.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useControls } from "@/video/state/logic/controls";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface MetaControllerProps {
|
||||
meta?: MWMediaMeta;
|
||||
}
|
||||
|
||||
export function MetaController(props: MetaControllerProps) {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const controls = useControls(descriptor);
|
||||
|
||||
useEffect(() => {
|
||||
controls.setMeta(props.meta);
|
||||
}, [props, controls]);
|
||||
|
||||
return null;
|
||||
}
|
35
src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts
Normal file
35
src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { MWMediaType } from "@/backend/metadata/types";
|
||||
import { useMeta } from "@/video/state/logic/meta";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
||||
const meta = useMeta(descriptor);
|
||||
|
||||
const currentSeasonInfo = useMemo(() => {
|
||||
return meta?.seasons?.find(
|
||||
(season) => season.id === meta?.episode?.seasonId
|
||||
);
|
||||
}, [meta]);
|
||||
|
||||
const currentEpisodeInfo = useMemo(() => {
|
||||
return currentSeasonInfo?.episodes?.find(
|
||||
(episode) => episode.id === meta?.episode?.episodeId
|
||||
);
|
||||
}, [currentSeasonInfo, meta]);
|
||||
|
||||
const isSeries = Boolean(
|
||||
meta?.meta?.type === MWMediaType.SERIES && meta?.episode
|
||||
);
|
||||
|
||||
if (!isSeries) return { isSeries: false };
|
||||
|
||||
const humanizedEpisodeId = `S${currentSeasonInfo?.number} E${currentEpisodeInfo?.number}`;
|
||||
|
||||
return {
|
||||
isSeries: true,
|
||||
humanizedEpisodeId,
|
||||
currentSeasonInfo,
|
||||
currentEpisodeInfo,
|
||||
meta: meta?.meta,
|
||||
};
|
||||
}
|
37
src/video/components/parts/VideoPlayerError.tsx
Normal file
37
src/video/components/parts/VideoPlayerError.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Title } from "@/components/text/Title";
|
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||
import { useMeta } from "@/video/state/logic/meta";
|
||||
import { ReactNode } from "react";
|
||||
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||
|
||||
interface VideoPlayerErrorProps {
|
||||
onGoBack?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function VideoPlayerError(props: VideoPlayerErrorProps) {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const meta = useMeta(descriptor);
|
||||
// TODO add error state
|
||||
|
||||
const err = null as any;
|
||||
|
||||
if (!err) return props.children as any;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-denim-100">
|
||||
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||
<Title>Failed to load media</Title>
|
||||
<p className="my-6 max-w-lg">
|
||||
{err?.name}: {err?.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2">
|
||||
<VideoPlayerHeader media={meta?.meta} onClick={props.onGoBack} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
65
src/video/components/parts/VideoPlayerHeader.tsx
Normal file
65
src/video/components/parts/VideoPlayerHeader.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import {
|
||||
getIfBookmarkedFromPortable,
|
||||
useBookmarkContext,
|
||||
} from "@/state/bookmark";
|
||||
|
||||
interface VideoPlayerHeaderProps {
|
||||
media?: MWMediaMeta;
|
||||
onClick?: () => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||
const { bookmarkStore, setItemBookmark } = useBookmarkContext();
|
||||
const isBookmarked = props.media
|
||||
? getIfBookmarkedFromPortable(bookmarkStore.bookmarks, props.media)
|
||||
: false;
|
||||
const showDivider = props.media && props.onClick;
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-1 items-center">
|
||||
<p className="flex items-center">
|
||||
{props.onClick ? (
|
||||
<span
|
||||
onClick={props.onClick}
|
||||
className="flex cursor-pointer items-center py-1 text-white opacity-50 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<Icon className="mr-2" icon={Icons.ARROW_LEFT} />
|
||||
<span>Back to home</span>
|
||||
</span>
|
||||
) : null}
|
||||
{showDivider ? (
|
||||
<span className="mx-4 h-6 w-[1.5px] rotate-[30deg] bg-white opacity-50" />
|
||||
) : null}
|
||||
{props.media ? (
|
||||
<span className="flex items-center text-white">
|
||||
<span>{props.media.title}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
{props.media && (
|
||||
<IconPatch
|
||||
clickable
|
||||
transparent
|
||||
icon={isBookmarked ? Icons.BOOKMARK : Icons.BOOKMARK_OUTLINE}
|
||||
className="ml-2 text-white"
|
||||
onClick={() =>
|
||||
props.media && setItemBookmark(props.media, !isBookmarked)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{props.showControls ? null : (
|
||||
// <>
|
||||
// <AirplayControl />
|
||||
// <ChromeCastControl />
|
||||
// </>
|
||||
<BrandPill />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,7 +2,8 @@ export type VideoPlayerEvent =
|
|||
| "mediaplaying"
|
||||
| "source"
|
||||
| "progress"
|
||||
| "interface";
|
||||
| "interface"
|
||||
| "meta";
|
||||
|
||||
function createEventString(id: string, event: VideoPlayerEvent): string {
|
||||
return `_vid:::${id}:::${event}`;
|
||||
|
|
|
@ -4,29 +4,38 @@ import { VideoPlayerState } from "./types";
|
|||
|
||||
function initPlayer(): VideoPlayerState {
|
||||
return {
|
||||
isPlaying: false,
|
||||
isPaused: true,
|
||||
isFullscreen: false,
|
||||
isFocused: false,
|
||||
isLoading: false,
|
||||
isSeeking: false,
|
||||
isFirstLoading: true,
|
||||
time: 0,
|
||||
duration: 0,
|
||||
interface: {
|
||||
popout: null,
|
||||
isFullscreen: false,
|
||||
isFocused: false,
|
||||
leftControlHovering: false,
|
||||
},
|
||||
|
||||
mediaPlaying: {
|
||||
isPlaying: false,
|
||||
isPaused: true,
|
||||
isLoading: false,
|
||||
isSeeking: false,
|
||||
isFirstLoading: true,
|
||||
hasPlayedOnce: false,
|
||||
},
|
||||
|
||||
progress: {
|
||||
time: 0,
|
||||
duration: 0,
|
||||
buffered: 0,
|
||||
},
|
||||
|
||||
meta: null,
|
||||
source: null,
|
||||
error: null,
|
||||
|
||||
volume: 0,
|
||||
buffered: 0,
|
||||
pausedWhenSeeking: false,
|
||||
hasInitialized: false,
|
||||
leftControlHovering: false,
|
||||
hasPlayedOnce: false,
|
||||
error: null,
|
||||
popout: null,
|
||||
seasonData: {
|
||||
isSeries: false,
|
||||
},
|
||||
canAirplay: false,
|
||||
|
||||
stateProvider: null,
|
||||
source: null,
|
||||
wrapperElement: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { updateInterface } from "@/video/state/logic/interface";
|
||||
import { updateMeta } from "@/video/state/logic/meta";
|
||||
import { getPlayerState } from "../cache";
|
||||
import { VideoPlayerStateController } from "../providers/providerTypes";
|
||||
|
||||
|
@ -7,6 +9,7 @@ type ControlMethods = {
|
|||
closePopout(): void;
|
||||
setLeftControlsHover(hovering: boolean): void;
|
||||
setFocused(focused: boolean): void;
|
||||
setMeta(meta?: MWMediaMeta): void;
|
||||
};
|
||||
|
||||
export function useControls(
|
||||
|
@ -40,20 +43,30 @@ export function useControls(
|
|||
|
||||
// other controls
|
||||
setLeftControlsHover(hovering) {
|
||||
state.leftControlHovering = hovering;
|
||||
state.interface.leftControlHovering = hovering;
|
||||
updateInterface(descriptor, state);
|
||||
},
|
||||
openPopout(id: string) {
|
||||
state.popout = id;
|
||||
state.interface.popout = id;
|
||||
updateInterface(descriptor, state);
|
||||
},
|
||||
closePopout() {
|
||||
state.popout = null;
|
||||
state.interface.popout = null;
|
||||
updateInterface(descriptor, state);
|
||||
},
|
||||
setFocused(focused) {
|
||||
state.isFocused = focused;
|
||||
state.interface.isFocused = focused;
|
||||
updateInterface(descriptor, state);
|
||||
},
|
||||
setMeta(meta) {
|
||||
if (!meta) {
|
||||
state.meta = null;
|
||||
} else {
|
||||
state.meta = {
|
||||
meta,
|
||||
};
|
||||
}
|
||||
updateMeta(descriptor, state);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@ export type VideoInterfaceEvent = {
|
|||
|
||||
function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent {
|
||||
return {
|
||||
popout: state.popout,
|
||||
leftControlHovering: state.leftControlHovering,
|
||||
isFocused: state.isFocused,
|
||||
isFullscreen: state.isFullscreen,
|
||||
popout: state.interface.popout,
|
||||
leftControlHovering: state.interface.leftControlHovering,
|
||||
isFocused: state.interface.isFocused,
|
||||
isFullscreen: state.interface.isFullscreen,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16,12 +16,12 @@ function getMediaPlayingFromState(
|
|||
state: VideoPlayerState
|
||||
): VideoMediaPlayingEvent {
|
||||
return {
|
||||
hasPlayedOnce: state.hasPlayedOnce,
|
||||
isLoading: state.isLoading,
|
||||
isPaused: state.isPaused,
|
||||
isPlaying: state.isPlaying,
|
||||
isSeeking: state.isSeeking,
|
||||
isFirstLoading: state.isFirstLoading,
|
||||
hasPlayedOnce: state.mediaPlaying.hasPlayedOnce,
|
||||
isLoading: state.mediaPlaying.isLoading,
|
||||
isPaused: state.mediaPlaying.isPaused,
|
||||
isPlaying: state.mediaPlaying.isPlaying,
|
||||
isSeeking: state.mediaPlaying.isSeeking,
|
||||
isFirstLoading: state.mediaPlaying.isFirstLoading,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
35
src/video/state/logic/meta.ts
Normal file
35
src/video/state/logic/meta.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { getPlayerState } from "../cache";
|
||||
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||
import { VideoPlayerMeta, VideoPlayerState } from "../types";
|
||||
|
||||
export type VideoMetaEvent = VideoPlayerMeta | null;
|
||||
|
||||
function getMetaFromState(state: VideoPlayerState): VideoMetaEvent {
|
||||
return state.meta
|
||||
? {
|
||||
...state.meta,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
export function updateMeta(descriptor: string, state: VideoPlayerState) {
|
||||
sendEvent<VideoMetaEvent>(descriptor, "meta", getMetaFromState(state));
|
||||
}
|
||||
|
||||
export function useMeta(descriptor: string): VideoMetaEvent {
|
||||
const state = getPlayerState(descriptor);
|
||||
const [data, setData] = useState<VideoMetaEvent>(getMetaFromState(state));
|
||||
|
||||
useEffect(() => {
|
||||
function update(payload: CustomEvent<VideoMetaEvent>) {
|
||||
setData(payload.detail);
|
||||
}
|
||||
listenEvent(descriptor, "meta", update);
|
||||
return () => {
|
||||
unlistenEvent(descriptor, "meta", update);
|
||||
};
|
||||
}, [descriptor]);
|
||||
|
||||
return data;
|
||||
}
|
|
@ -11,9 +11,9 @@ export type VideoProgressEvent = {
|
|||
|
||||
function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
|
||||
return {
|
||||
time: state.time,
|
||||
duration: state.duration,
|
||||
buffered: state.buffered,
|
||||
time: state.progress.time,
|
||||
duration: state.progress.duration,
|
||||
buffered: state.progress.buffered,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from "@/utils/detectFeatures";
|
||||
import { MWStreamType } from "@/backend/helpers/streams";
|
||||
import { updateInterface } from "@/video/state/logic/interface";
|
||||
import { updateSource } from "@/video/state/logic/source";
|
||||
import { getPlayerState } from "../cache";
|
||||
import { updateMediaPlaying } from "../logic/mediaplaying";
|
||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||
|
@ -51,7 +52,7 @@ export function createVideoStateProvider(
|
|||
|
||||
// update state
|
||||
player.currentTime = time;
|
||||
state.time = time;
|
||||
state.progress.time = time;
|
||||
updateProgress(descriptor, state);
|
||||
},
|
||||
setSeeking(active) {
|
||||
|
@ -63,12 +64,14 @@ export function createVideoStateProvider(
|
|||
|
||||
// when seeking we pause the video
|
||||
// this variables isnt reactive, just used so the state can be remembered next unseek
|
||||
state.pausedWhenSeeking = state.isPaused;
|
||||
state.pausedWhenSeeking = state.mediaPlaying.isPaused;
|
||||
this.pause();
|
||||
},
|
||||
setSource(source) {
|
||||
if (!source) {
|
||||
player.src = "";
|
||||
state.source = null;
|
||||
updateSource(descriptor, state);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -105,52 +108,63 @@ export function createVideoStateProvider(
|
|||
} else if (source.type === MWStreamType.MP4) {
|
||||
player.src = source.source;
|
||||
}
|
||||
|
||||
// update state
|
||||
state.source = {
|
||||
quality: source.quality,
|
||||
type: source.type,
|
||||
url: source.source,
|
||||
};
|
||||
updateSource(descriptor, state);
|
||||
},
|
||||
providerStart() {
|
||||
// TODO stored volume
|
||||
const pause = () => {
|
||||
state.isPaused = true;
|
||||
state.isPlaying = false;
|
||||
state.mediaPlaying.isPaused = true;
|
||||
state.mediaPlaying.isPlaying = false;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const playing = () => {
|
||||
state.isPaused = false;
|
||||
state.isPlaying = true;
|
||||
state.isLoading = false;
|
||||
state.hasPlayedOnce = true;
|
||||
state.mediaPlaying.isPaused = false;
|
||||
state.mediaPlaying.isPlaying = true;
|
||||
state.mediaPlaying.isLoading = false;
|
||||
state.mediaPlaying.hasPlayedOnce = true;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const waiting = () => {
|
||||
state.isLoading = true;
|
||||
state.mediaPlaying.isLoading = true;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const seeking = () => {
|
||||
state.isSeeking = true;
|
||||
state.mediaPlaying.isSeeking = true;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const seeked = () => {
|
||||
state.isSeeking = false;
|
||||
state.mediaPlaying.isSeeking = false;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const loadedmetadata = () => {
|
||||
state.duration = player.duration;
|
||||
state.progress.duration = player.duration;
|
||||
updateProgress(descriptor, state);
|
||||
};
|
||||
const timeupdate = () => {
|
||||
state.duration = player.duration;
|
||||
state.time = player.currentTime;
|
||||
state.progress.duration = player.duration;
|
||||
state.progress.time = player.currentTime;
|
||||
updateProgress(descriptor, state);
|
||||
};
|
||||
const progress = () => {
|
||||
state.buffered = handleBuffered(player.currentTime, player.buffered);
|
||||
state.progress.buffered = handleBuffered(
|
||||
player.currentTime,
|
||||
player.buffered
|
||||
);
|
||||
updateProgress(descriptor, state);
|
||||
};
|
||||
const canplay = () => {
|
||||
state.isFirstLoading = false;
|
||||
state.mediaPlaying.isFirstLoading = false;
|
||||
updateMediaPlaying(descriptor, state);
|
||||
};
|
||||
const fullscreenchange = () => {
|
||||
state.isFullscreen = !!document.fullscreenElement;
|
||||
state.interface.isFullscreen = !!document.fullscreenElement;
|
||||
updateInterface(descriptor, state);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,47 +1,66 @@
|
|||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
||||
|
||||
export type VideoPlayerMeta = {
|
||||
meta: MWMediaMeta;
|
||||
episode?: {
|
||||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
seasons?: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
episodes?: { id: string; number: number; title: string }[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export type VideoPlayerState = {
|
||||
isPlaying: boolean;
|
||||
isPaused: boolean;
|
||||
isSeeking: boolean;
|
||||
isLoading: boolean;
|
||||
isFirstLoading: boolean;
|
||||
isFullscreen: boolean;
|
||||
time: number;
|
||||
duration: number;
|
||||
volume: number;
|
||||
buffered: number;
|
||||
pausedWhenSeeking: boolean;
|
||||
hasInitialized: boolean;
|
||||
leftControlHovering: boolean;
|
||||
hasPlayedOnce: boolean;
|
||||
popout: string | null;
|
||||
isFocused: boolean;
|
||||
seasonData: {
|
||||
isSeries: boolean;
|
||||
current?: {
|
||||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
seasons?: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
episodes?: { id: string; number: number; title: string }[];
|
||||
}[];
|
||||
// state related to the user interface
|
||||
interface: {
|
||||
isFullscreen: boolean;
|
||||
popout: string | null; // id of current popout (eg source select, episode select)
|
||||
isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused)
|
||||
leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
|
||||
};
|
||||
|
||||
error: null | {
|
||||
name: string;
|
||||
description: string;
|
||||
// state related to the playing state of the media
|
||||
mediaPlaying: {
|
||||
isPlaying: boolean;
|
||||
isPaused: boolean;
|
||||
isSeeking: boolean; // seeking with progress bar
|
||||
isLoading: boolean; // buffering or not
|
||||
isFirstLoading: boolean; // first buffering of the video, used to show
|
||||
hasPlayedOnce: boolean; // has the video played at all?
|
||||
};
|
||||
canAirplay: boolean;
|
||||
stateProvider: VideoPlayerStateProvider | null;
|
||||
|
||||
// state related to video progress
|
||||
progress: {
|
||||
time: number;
|
||||
duration: number;
|
||||
buffered: number;
|
||||
};
|
||||
|
||||
// meta data of video
|
||||
meta: null | VideoPlayerMeta;
|
||||
source: null | {
|
||||
quality: MWStreamQuality;
|
||||
url: string;
|
||||
type: MWStreamType;
|
||||
};
|
||||
error: null | {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// misc
|
||||
volume: number;
|
||||
pausedWhenSeeking: boolean;
|
||||
hasInitialized: boolean;
|
||||
canAirplay: boolean;
|
||||
|
||||
// backing fields
|
||||
stateProvider: VideoPlayerStateProvider | null;
|
||||
wrapperElement: HTMLDivElement | null;
|
||||
};
|
||||
|
|
|
@ -5,15 +5,10 @@
|
|||
// import { useEffect, useRef } from "react";
|
||||
|
||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||
import { LoadingAction } from "@/video/components/actions/LoadingAction";
|
||||
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
|
||||
import { PauseAction } from "@/video/components/actions/PauseAction";
|
||||
import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
||||
import { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
|
||||
import { TimeAction } from "@/video/components/actions/TimeAction";
|
||||
import { MWMediaType } from "@/backend/metadata/types";
|
||||
import { MetaController } from "@/video/components/controllers/MetaController";
|
||||
import { SourceController } from "@/video/components/controllers/SourceController";
|
||||
import { VideoPlayer } from "@/video/components/VideoPlayer";
|
||||
import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
|
||||
|
||||
// function ChromeCastButton() {
|
||||
// const ref = useRef<HTMLDivElement>(null);
|
||||
|
@ -31,12 +26,21 @@ import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
|
|||
|
||||
export function TestView() {
|
||||
return (
|
||||
<VideoPlayer>
|
||||
<VideoPlayer onGoBack={() => alert("hello world")}>
|
||||
<SourceController
|
||||
quality={MWStreamQuality.QUNKNOWN}
|
||||
source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
||||
type={MWStreamType.MP4}
|
||||
/>
|
||||
<MetaController
|
||||
meta={{
|
||||
id: "test",
|
||||
title: "Hello world",
|
||||
type: MWMediaType.MOVIE,
|
||||
year: "1234",
|
||||
seasons: undefined,
|
||||
}}
|
||||
/>
|
||||
</VideoPlayer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { DecoratedVideoPlayer } from "@/../__old/DecoratedVideoPlayer";
|
||||
import { MWStream } from "@/backend/helpers/streams";
|
||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||
import { VideoPlayerHeader } from "@/../__old/parts/VideoPlayerHeader";
|
||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
||||
import { SourceControl } from "@/../__old/controls/SourceControl";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
import { MWMediaType } from "@/backend/metadata/types";
|
||||
import { useGoBack } from "@/hooks/useGoBack";
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { VideoPlayer } from "@/video/components/VideoPlayer";
|
||||
import { MetaController } from "@/video/components/controllers/MetaController";
|
||||
import { SourceController } from "@/video/components/controllers/SourceController";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
||||
import { useWatchedItem } from "@/state/watched";
|
||||
import { ProgressListenerControl } from "@/../__old/controls/ProgressListenerControl";
|
||||
import { ShowControl } from "@/../__old/controls/ShowControl";
|
||||
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
||||
|
@ -113,17 +112,18 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|||
<Helmet>
|
||||
<html data-full="true" />
|
||||
</Helmet>
|
||||
<DecoratedVideoPlayer media={props.meta} onGoBack={goBack} autoPlay>
|
||||
<SourceControl
|
||||
<VideoPlayer onGoBack={goBack}>
|
||||
<MetaController meta={props.meta.meta} />
|
||||
<SourceController
|
||||
source={props.stream.streamUrl}
|
||||
type={props.stream.type}
|
||||
quality={props.stream.quality}
|
||||
/>
|
||||
<ProgressListenerControl
|
||||
{/* <ProgressListenerControl
|
||||
startAt={firstStartTime.current}
|
||||
onProgress={updateProgress}
|
||||
/>
|
||||
{props.selected.type === MWMediaType.SERIES &&
|
||||
/> */}
|
||||
{/* {props.selected.type === MWMediaType.SERIES &&
|
||||
props.meta.meta.type === MWMediaType.SERIES ? (
|
||||
<ShowControl
|
||||
series={{
|
||||
|
@ -138,8 +138,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|||
seasonData={props.meta.meta.seasonData}
|
||||
seasons={props.meta.meta.seasons}
|
||||
/>
|
||||
) : null}
|
||||
</DecoratedVideoPlayer>
|
||||
) : null} */}
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue