meta data in video player

This commit is contained in:
Jelle van Snik 2023-02-04 16:29:21 +01:00
parent 27ef9be6b1
commit bb14d63a9c
20 changed files with 492 additions and 168 deletions

View File

@ -2,13 +2,18 @@ import { Transition } from "@/components/Transition";
import { useIsMobile } from "@/hooks/useIsMobile"; import { useIsMobile } from "@/hooks/useIsMobile";
import { BackdropAction } from "@/video/components/actions/BackdropAction"; import { BackdropAction } from "@/video/components/actions/BackdropAction";
import { FullscreenAction } from "@/video/components/actions/FullscreenAction"; import { FullscreenAction } from "@/video/components/actions/FullscreenAction";
import { HeaderAction } from "@/video/components/actions/HeaderAction";
import { LoadingAction } from "@/video/components/actions/LoadingAction"; import { LoadingAction } from "@/video/components/actions/LoadingAction";
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction"; import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction";
import { MobileCenterAction } from "@/video/components/actions/MobileCenterAction"; import { MobileCenterAction } from "@/video/components/actions/MobileCenterAction";
import { PageTitleAction } from "@/video/components/actions/PageTitleAction";
import { PauseAction } from "@/video/components/actions/PauseAction"; import { PauseAction } from "@/video/components/actions/PauseAction";
import { ProgressAction } from "@/video/components/actions/ProgressAction"; 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 { SkipTimeAction } from "@/video/components/actions/SkipTimeAction";
import { TimeAction } from "@/video/components/actions/TimeAction"; import { TimeAction } from "@/video/components/actions/TimeAction";
import { VideoPlayerError } from "@/video/components/parts/VideoPlayerError";
import { import {
VideoPlayerBase, VideoPlayerBase,
VideoPlayerBaseProps, VideoPlayerBaseProps,
@ -17,6 +22,10 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useControls } from "@/video/state/logic/controls"; import { useControls } from "@/video/state/logic/controls";
import { ReactNode, useCallback, useState } from "react"; import { ReactNode, useCallback, useState } from "react";
type Props = VideoPlayerBaseProps & {
onGoBack?: () => void;
};
function CenterPosition(props: { children: ReactNode }) { function CenterPosition(props: { children: ReactNode }) {
return ( return (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
@ -48,12 +57,12 @@ function LeftSideControls() {
{/* <VolumeControl className="mr-2" /> */} {/* <VolumeControl className="mr-2" /> */}
<TimeAction /> <TimeAction />
</div> </div>
{/* <ShowTitleControl /> */} <ShowTitleAction />
</> </>
); );
} }
export function VideoPlayer(props: VideoPlayerBaseProps) { export function VideoPlayer(props: Props) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const { isMobile } = useIsMobile(); const { isMobile } = useIsMobile();
@ -65,76 +74,72 @@ export function VideoPlayer(props: VideoPlayerBaseProps) {
); );
// TODO autoplay // TODO autoplay
// TODO meta data // TODO safe area only if full screen or fill screen
return ( return (
<VideoPlayerBase> <VideoPlayerBase>
{/* <PageTitleControl media={props.media?.meta} /> */} <PageTitleAction />
{/* <VideoPlayerError media={props.media?.meta} onGoBack={props.onGoBack}> */} <VideoPlayerError onGoBack={props.onGoBack}>
<BackdropAction onBackdropChange={onBackdropChange}> <BackdropAction onBackdropChange={onBackdropChange}>
<CenterPosition> <CenterPosition>
<LoadingAction /> <LoadingAction />
</CenterPosition> </CenterPosition>
<CenterPosition> <CenterPosition>
<MiddlePauseAction /> <MiddlePauseAction />
</CenterPosition> </CenterPosition>
{isMobile ? ( {isMobile ? (
<Transition
animation="fade"
show={show}
className="absolute inset-0 flex items-center justify-center"
>
<MobileCenterAction />
</Transition>
) : (
""
)}
<Transition <Transition
animation="fade" animation="slide-down"
show={show} 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>
) : ( <Transition
"" animation="slide-up"
)} show={show}
<Transition className="pointer-events-auto absolute inset-x-0 bottom-0 flex flex-col px-4 pb-2 [margin-bottom:env(safe-area-inset-bottom)]"
animation="slide-down" >
show={show} <div className="flex w-full items-center space-x-3">
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2" {isMobile && <TimeAction noDuration />}
> <ProgressAction />
{/* <VideoPlayerHeader </div>
media={props.media?.meta} <div className="flex items-center">
onClick={props.onGoBack} {isMobile ? (
isMobile={isMobile} <div className="grid w-full grid-cols-[56px,1fr,56px] items-center">
/> */} <div />
</Transition> <div className="flex items-center justify-center">
<Transition {/* <SeriesSelectionControl /> */}
animation="slide-up" {/* <SourceSelectionControl media={props.media} /> */}
show={show} </div>
className="pointer-events-auto absolute inset-x-0 bottom-0 flex flex-col px-4 pb-2 [margin-bottom:env(safe-area-inset-bottom)]" <FullscreenAction />
>
<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> </div>
<FullscreenAction /> ) : (
</div> <>
) : ( <LeftSideControls />
<> <div className="flex-1" />
<LeftSideControls /> <QualityDisplayAction />
<div className="flex-1" /> {/* <SeriesSelectionControl />
{/* <QualityDisplayControl />
<SeriesSelectionControl />
<SourceSelectionControl media={props.media} /> <SourceSelectionControl media={props.media} />
<AirplayControl /> <AirplayControl />
<ChromeCastControl /> */} <ChromeCastControl /> */}
<FullscreenAction /> <FullscreenAction />
</> </>
)} )}
</div> </div>
</Transition> </Transition>
</BackdropAction> </BackdropAction>
{props.children} {props.children}
{/* </VideoPlayerError> */} </VideoPlayerError>
</VideoPlayerBase> </VideoPlayerBase>
); );
} }

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

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

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

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

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

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

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

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

View File

@ -2,7 +2,8 @@ export type VideoPlayerEvent =
| "mediaplaying" | "mediaplaying"
| "source" | "source"
| "progress" | "progress"
| "interface"; | "interface"
| "meta";
function createEventString(id: string, event: VideoPlayerEvent): string { function createEventString(id: string, event: VideoPlayerEvent): string {
return `_vid:::${id}:::${event}`; return `_vid:::${id}:::${event}`;

View File

@ -4,29 +4,38 @@ import { VideoPlayerState } from "./types";
function initPlayer(): VideoPlayerState { function initPlayer(): VideoPlayerState {
return { return {
isPlaying: false, interface: {
isPaused: true, popout: null,
isFullscreen: false, isFullscreen: false,
isFocused: false, isFocused: false,
isLoading: false, leftControlHovering: false,
isSeeking: false, },
isFirstLoading: true,
time: 0, mediaPlaying: {
duration: 0, 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, volume: 0,
buffered: 0,
pausedWhenSeeking: false, pausedWhenSeeking: false,
hasInitialized: false, hasInitialized: false,
leftControlHovering: false,
hasPlayedOnce: false,
error: null,
popout: null,
seasonData: {
isSeries: false,
},
canAirplay: false, canAirplay: false,
stateProvider: null, stateProvider: null,
source: null,
wrapperElement: null, wrapperElement: null,
}; };
} }

View File

@ -1,4 +1,6 @@
import { MWMediaMeta } from "@/backend/metadata/types";
import { updateInterface } from "@/video/state/logic/interface"; import { updateInterface } from "@/video/state/logic/interface";
import { updateMeta } from "@/video/state/logic/meta";
import { getPlayerState } from "../cache"; import { getPlayerState } from "../cache";
import { VideoPlayerStateController } from "../providers/providerTypes"; import { VideoPlayerStateController } from "../providers/providerTypes";
@ -7,6 +9,7 @@ type ControlMethods = {
closePopout(): void; closePopout(): void;
setLeftControlsHover(hovering: boolean): void; setLeftControlsHover(hovering: boolean): void;
setFocused(focused: boolean): void; setFocused(focused: boolean): void;
setMeta(meta?: MWMediaMeta): void;
}; };
export function useControls( export function useControls(
@ -40,20 +43,30 @@ export function useControls(
// other controls // other controls
setLeftControlsHover(hovering) { setLeftControlsHover(hovering) {
state.leftControlHovering = hovering; state.interface.leftControlHovering = hovering;
updateInterface(descriptor, state); updateInterface(descriptor, state);
}, },
openPopout(id: string) { openPopout(id: string) {
state.popout = id; state.interface.popout = id;
updateInterface(descriptor, state); updateInterface(descriptor, state);
}, },
closePopout() { closePopout() {
state.popout = null; state.interface.popout = null;
updateInterface(descriptor, state); updateInterface(descriptor, state);
}, },
setFocused(focused) { setFocused(focused) {
state.isFocused = focused; state.interface.isFocused = focused;
updateInterface(descriptor, state); updateInterface(descriptor, state);
}, },
setMeta(meta) {
if (!meta) {
state.meta = null;
} else {
state.meta = {
meta,
};
}
updateMeta(descriptor, state);
},
}; };
} }

View File

@ -12,10 +12,10 @@ export type VideoInterfaceEvent = {
function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent { function getInterfaceFromState(state: VideoPlayerState): VideoInterfaceEvent {
return { return {
popout: state.popout, popout: state.interface.popout,
leftControlHovering: state.leftControlHovering, leftControlHovering: state.interface.leftControlHovering,
isFocused: state.isFocused, isFocused: state.interface.isFocused,
isFullscreen: state.isFullscreen, isFullscreen: state.interface.isFullscreen,
}; };
} }

View File

@ -16,12 +16,12 @@ function getMediaPlayingFromState(
state: VideoPlayerState state: VideoPlayerState
): VideoMediaPlayingEvent { ): VideoMediaPlayingEvent {
return { return {
hasPlayedOnce: state.hasPlayedOnce, hasPlayedOnce: state.mediaPlaying.hasPlayedOnce,
isLoading: state.isLoading, isLoading: state.mediaPlaying.isLoading,
isPaused: state.isPaused, isPaused: state.mediaPlaying.isPaused,
isPlaying: state.isPlaying, isPlaying: state.mediaPlaying.isPlaying,
isSeeking: state.isSeeking, isSeeking: state.mediaPlaying.isSeeking,
isFirstLoading: state.isFirstLoading, isFirstLoading: state.mediaPlaying.isFirstLoading,
}; };
} }

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

View File

@ -11,9 +11,9 @@ export type VideoProgressEvent = {
function getProgressFromState(state: VideoPlayerState): VideoProgressEvent { function getProgressFromState(state: VideoPlayerState): VideoProgressEvent {
return { return {
time: state.time, time: state.progress.time,
duration: state.duration, duration: state.progress.duration,
buffered: state.buffered, buffered: state.progress.buffered,
}; };
} }

View File

@ -7,6 +7,7 @@ import {
} from "@/utils/detectFeatures"; } from "@/utils/detectFeatures";
import { MWStreamType } from "@/backend/helpers/streams"; import { MWStreamType } from "@/backend/helpers/streams";
import { updateInterface } from "@/video/state/logic/interface"; import { updateInterface } from "@/video/state/logic/interface";
import { updateSource } from "@/video/state/logic/source";
import { getPlayerState } from "../cache"; import { getPlayerState } from "../cache";
import { updateMediaPlaying } from "../logic/mediaplaying"; import { updateMediaPlaying } from "../logic/mediaplaying";
import { VideoPlayerStateProvider } from "./providerTypes"; import { VideoPlayerStateProvider } from "./providerTypes";
@ -51,7 +52,7 @@ export function createVideoStateProvider(
// update state // update state
player.currentTime = time; player.currentTime = time;
state.time = time; state.progress.time = time;
updateProgress(descriptor, state); updateProgress(descriptor, state);
}, },
setSeeking(active) { setSeeking(active) {
@ -63,12 +64,14 @@ export function createVideoStateProvider(
// when seeking we pause the video // when seeking we pause the video
// this variables isnt reactive, just used so the state can be remembered next unseek // 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(); this.pause();
}, },
setSource(source) { setSource(source) {
if (!source) { if (!source) {
player.src = ""; player.src = "";
state.source = null;
updateSource(descriptor, state);
return; return;
} }
@ -105,52 +108,63 @@ export function createVideoStateProvider(
} else if (source.type === MWStreamType.MP4) { } else if (source.type === MWStreamType.MP4) {
player.src = source.source; player.src = source.source;
} }
// update state
state.source = {
quality: source.quality,
type: source.type,
url: source.source,
};
updateSource(descriptor, state);
}, },
providerStart() { providerStart() {
// TODO stored volume // TODO stored volume
const pause = () => { const pause = () => {
state.isPaused = true; state.mediaPlaying.isPaused = true;
state.isPlaying = false; state.mediaPlaying.isPlaying = false;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const playing = () => { const playing = () => {
state.isPaused = false; state.mediaPlaying.isPaused = false;
state.isPlaying = true; state.mediaPlaying.isPlaying = true;
state.isLoading = false; state.mediaPlaying.isLoading = false;
state.hasPlayedOnce = true; state.mediaPlaying.hasPlayedOnce = true;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const waiting = () => { const waiting = () => {
state.isLoading = true; state.mediaPlaying.isLoading = true;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const seeking = () => { const seeking = () => {
state.isSeeking = true; state.mediaPlaying.isSeeking = true;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const seeked = () => { const seeked = () => {
state.isSeeking = false; state.mediaPlaying.isSeeking = false;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const loadedmetadata = () => { const loadedmetadata = () => {
state.duration = player.duration; state.progress.duration = player.duration;
updateProgress(descriptor, state); updateProgress(descriptor, state);
}; };
const timeupdate = () => { const timeupdate = () => {
state.duration = player.duration; state.progress.duration = player.duration;
state.time = player.currentTime; state.progress.time = player.currentTime;
updateProgress(descriptor, state); updateProgress(descriptor, state);
}; };
const progress = () => { const progress = () => {
state.buffered = handleBuffered(player.currentTime, player.buffered); state.progress.buffered = handleBuffered(
player.currentTime,
player.buffered
);
updateProgress(descriptor, state); updateProgress(descriptor, state);
}; };
const canplay = () => { const canplay = () => {
state.isFirstLoading = false; state.mediaPlaying.isFirstLoading = false;
updateMediaPlaying(descriptor, state); updateMediaPlaying(descriptor, state);
}; };
const fullscreenchange = () => { const fullscreenchange = () => {
state.isFullscreen = !!document.fullscreenElement; state.interface.isFullscreen = !!document.fullscreenElement;
updateInterface(descriptor, state); updateInterface(descriptor, state);
}; };

View File

@ -1,47 +1,66 @@
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
import { MWMediaMeta } from "@/backend/metadata/types";
import { VideoPlayerStateProvider } from "./providers/providerTypes"; 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 = { export type VideoPlayerState = {
isPlaying: boolean; // state related to the user interface
isPaused: boolean; interface: {
isSeeking: boolean; isFullscreen: boolean;
isLoading: boolean; popout: string | null; // id of current popout (eg source select, episode select)
isFirstLoading: boolean; isFocused: boolean; // is the video player the users focus? (shortcuts only works when its focused)
isFullscreen: boolean; leftControlHovering: boolean; // is the cursor hovered over the left side of player controls
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 }[];
}[];
}; };
error: null | { // state related to the playing state of the media
name: string; mediaPlaying: {
description: string; 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 | { source: null | {
quality: MWStreamQuality; quality: MWStreamQuality;
url: string; url: string;
type: MWStreamType; 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; wrapperElement: HTMLDivElement | null;
}; };

View File

@ -5,15 +5,10 @@
// import { useEffect, useRef } from "react"; // import { useEffect, useRef } from "react";
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
import { LoadingAction } from "@/video/components/actions/LoadingAction"; import { MWMediaType } from "@/backend/metadata/types";
import { MiddlePauseAction } from "@/video/components/actions/MiddlePauseAction"; import { MetaController } from "@/video/components/controllers/MetaController";
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 { SourceController } from "@/video/components/controllers/SourceController"; import { SourceController } from "@/video/components/controllers/SourceController";
import { VideoPlayer } from "@/video/components/VideoPlayer"; import { VideoPlayer } from "@/video/components/VideoPlayer";
import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
// function ChromeCastButton() { // function ChromeCastButton() {
// const ref = useRef<HTMLDivElement>(null); // const ref = useRef<HTMLDivElement>(null);
@ -31,12 +26,21 @@ import { VideoPlayerBase } from "@/video/components/VideoPlayerBase";
export function TestView() { export function TestView() {
return ( return (
<VideoPlayer> <VideoPlayer onGoBack={() => alert("hello world")}>
<SourceController <SourceController
quality={MWStreamQuality.QUNKNOWN} quality={MWStreamQuality.QUNKNOWN}
source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
type={MWStreamType.MP4} type={MWStreamType.MP4}
/> />
<MetaController
meta={{
id: "test",
title: "Hello world",
type: MWMediaType.MOVIE,
year: "1234",
seasons: undefined,
}}
/>
</VideoPlayer> </VideoPlayer>
); );
} }

View File

@ -1,22 +1,21 @@
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 "@/../__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 "@/../__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 "@/../__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";
import { useGoBack } from "@/hooks/useGoBack"; import { useGoBack } from "@/hooks/useGoBack";
import { IconPatch } from "@/components/buttons/IconPatch"; 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 { Icons } from "@/components/Icon";
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
import { useWatchedItem } from "@/state/watched"; import { useWatchedItem } from "@/state/watched";
import { ProgressListenerControl } from "@/../__old/controls/ProgressListenerControl";
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";
@ -113,17 +112,18 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
<Helmet> <Helmet>
<html data-full="true" /> <html data-full="true" />
</Helmet> </Helmet>
<DecoratedVideoPlayer media={props.meta} onGoBack={goBack} autoPlay> <VideoPlayer onGoBack={goBack}>
<SourceControl <MetaController meta={props.meta.meta} />
<SourceController
source={props.stream.streamUrl} source={props.stream.streamUrl}
type={props.stream.type} type={props.stream.type}
quality={props.stream.quality} quality={props.stream.quality}
/> />
<ProgressListenerControl {/* <ProgressListenerControl
startAt={firstStartTime.current} startAt={firstStartTime.current}
onProgress={updateProgress} onProgress={updateProgress}
/> /> */}
{props.selected.type === MWMediaType.SERIES && {/* {props.selected.type === MWMediaType.SERIES &&
props.meta.meta.type === MWMediaType.SERIES ? ( props.meta.meta.type === MWMediaType.SERIES ? (
<ShowControl <ShowControl
series={{ series={{
@ -138,8 +138,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
seasonData={props.meta.meta.seasonData} seasonData={props.meta.meta.seasonData}
seasons={props.meta.meta.seasons} seasons={props.meta.meta.seasons}
/> />
) : null} ) : null} */}
</DecoratedVideoPlayer> </VideoPlayer>
</div> </div>
); );
} }