mirror of
https://github.com/sussy-code/smov.git
synced 2025-01-01 16:37:39 +01:00
shortcuts, progress saving fix, error handling, airplay, safe are for full screen only
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
487ba39bbf
commit
76e4bc5851
23 changed files with 510 additions and 173 deletions
|
@ -31,70 +31,6 @@ const VideoPlayerInternals = forwardRef<
|
||||||
didInitialize.current = value;
|
didInitialize.current = value;
|
||||||
}, [didInitialize, video]);
|
}, [didInitialize, video]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isRolling = false;
|
|
||||||
const onKeyDown = (evt: KeyboardEvent) => {
|
|
||||||
if (!videoState.isFocused) return;
|
|
||||||
if (!ref || !(ref as any)?.current) return;
|
|
||||||
const el = (ref as any).current as HTMLVideoElement;
|
|
||||||
|
|
||||||
switch (evt.key.toLowerCase()) {
|
|
||||||
// Toggle fullscreen
|
|
||||||
case "f":
|
|
||||||
if (videoState.isFullscreen) {
|
|
||||||
videoState.exitFullscreen();
|
|
||||||
} else {
|
|
||||||
videoState.enterFullscreen();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Skip backwards
|
|
||||||
case "arrowleft":
|
|
||||||
videoState.setTime(videoState.time - 5);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Skip forward
|
|
||||||
case "arrowright":
|
|
||||||
videoState.setTime(videoState.time + 5);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Pause / play
|
|
||||||
case " ":
|
|
||||||
if (videoState.isPaused) {
|
|
||||||
videoState.play();
|
|
||||||
} else {
|
|
||||||
videoState.pause();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Mute
|
|
||||||
case "m":
|
|
||||||
toggleVolume();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Do a barrel Roll!
|
|
||||||
case "r":
|
|
||||||
if (isRolling) return;
|
|
||||||
isRolling = true;
|
|
||||||
el.classList.add("roll");
|
|
||||||
setTimeout(() => {
|
|
||||||
isRolling = false;
|
|
||||||
el.classList.remove("roll");
|
|
||||||
}, 1000);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
|
||||||
};
|
|
||||||
}, [videoState, toggleVolume, ref]);
|
|
||||||
|
|
||||||
// muted attribute is required for safari, as they cant change the volume itself
|
// muted attribute is required for safari, as they cant change the volume itself
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
|
|
|
@ -14,7 +14,11 @@ import { VideoProgressStore } from "./store";
|
||||||
const FIVETEEN_MINUTES = 15 * 60;
|
const FIVETEEN_MINUTES = 15 * 60;
|
||||||
const FIVE_MINUTES = 5 * 60;
|
const FIVE_MINUTES = 5 * 60;
|
||||||
|
|
||||||
function shouldSave(time: number, duration: number): boolean {
|
function shouldSave(
|
||||||
|
time: number,
|
||||||
|
duration: number,
|
||||||
|
isSeries: boolean
|
||||||
|
): boolean {
|
||||||
const timeFromEnd = Math.max(0, duration - time);
|
const timeFromEnd = Math.max(0, duration - time);
|
||||||
|
|
||||||
// short movie
|
// short movie
|
||||||
|
@ -26,7 +30,7 @@ function shouldSave(time: number, duration: number): boolean {
|
||||||
|
|
||||||
// long movie
|
// long movie
|
||||||
if (time < 30) return false;
|
if (time < 30) return false;
|
||||||
if (timeFromEnd < FIVE_MINUTES) return false;
|
if (timeFromEnd < FIVE_MINUTES && !isSeries) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,9 +130,10 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
||||||
// update actual item
|
// update actual item
|
||||||
item.progress = progress;
|
item.progress = progress;
|
||||||
item.percentage = Math.round((progress / total) * 100);
|
item.percentage = Math.round((progress / total) * 100);
|
||||||
|
item.watchedAt = Date.now();
|
||||||
|
|
||||||
// remove item if shouldnt save
|
// remove item if shouldnt save
|
||||||
if (!shouldSave(progress, total)) {
|
if (!shouldSave(progress, total, !!media.series)) {
|
||||||
newData.items = data.items.filter(
|
newData.items = data.items.filter(
|
||||||
(v) => !isSameEpisode(v.item, media)
|
(v) => !isSameEpisode(v.item, media)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
||||||
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 { HeaderAction } from "@/video/components/actions/HeaderAction";
|
||||||
|
@ -12,6 +13,7 @@ import { ProgressAction } from "@/video/components/actions/ProgressAction";
|
||||||
import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction";
|
import { QualityDisplayAction } from "@/video/components/actions/QualityDisplayAction";
|
||||||
import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction";
|
import { SeriesSelectionAction } from "@/video/components/actions/SeriesSelectionAction";
|
||||||
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
|
import { ShowTitleAction } from "@/video/components/actions/ShowTitleAction";
|
||||||
|
import { KeyboardShortcutsAction } from "@/video/components/actions/KeyboardShortcutsAction";
|
||||||
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 { VolumeAction } from "@/video/components/actions/VolumeAction";
|
import { VolumeAction } from "@/video/components/actions/VolumeAction";
|
||||||
|
@ -24,9 +26,7 @@ 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 & {
|
type Props = VideoPlayerBaseProps;
|
||||||
onGoBack?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function CenterPosition(props: { children: ReactNode }) {
|
function CenterPosition(props: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -75,74 +75,89 @@ export function VideoPlayer(props: Props) {
|
||||||
[setShow]
|
[setShow]
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO safe area only if full screen or fill screen
|
|
||||||
// TODO airplay
|
|
||||||
// TODO source selection
|
// TODO source selection
|
||||||
return (
|
return (
|
||||||
<VideoPlayerBase autoPlay={props.autoPlay}>
|
<VideoPlayerBase
|
||||||
<PageTitleAction />
|
autoPlay={props.autoPlay}
|
||||||
<VideoPlayerError onGoBack={props.onGoBack}>
|
includeSafeArea={props.includeSafeArea}
|
||||||
<BackdropAction onBackdropChange={onBackdropChange}>
|
onGoBack={props.onGoBack}
|
||||||
<CenterPosition>
|
>
|
||||||
<LoadingAction />
|
{({ isFullscreen }) => (
|
||||||
</CenterPosition>
|
<>
|
||||||
<CenterPosition>
|
<KeyboardShortcutsAction />
|
||||||
<MiddlePauseAction />
|
<PageTitleAction />
|
||||||
</CenterPosition>
|
<VideoPlayerError onGoBack={props.onGoBack}>
|
||||||
{isMobile ? (
|
<BackdropAction onBackdropChange={onBackdropChange}>
|
||||||
<Transition
|
<CenterPosition>
|
||||||
animation="fade"
|
<LoadingAction />
|
||||||
show={show}
|
</CenterPosition>
|
||||||
className="absolute inset-0 flex items-center justify-center"
|
<CenterPosition>
|
||||||
>
|
<MiddlePauseAction />
|
||||||
<MobileCenterAction />
|
</CenterPosition>
|
||||||
</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"
|
|
||||||
>
|
|
||||||
<HeaderAction showControls={isMobile} onClick={props.onGoBack} />
|
|
||||||
</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 ? (
|
{isMobile ? (
|
||||||
<div className="grid w-full grid-cols-[56px,1fr,56px] items-center">
|
<Transition
|
||||||
<div />
|
animation="fade"
|
||||||
<div className="flex items-center justify-center">
|
show={show}
|
||||||
<SeriesSelectionAction />
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
{/* <SourceSelectionControl media={props.media} /> */}
|
>
|
||||||
</div>
|
<MobileCenterAction />
|
||||||
<FullscreenAction />
|
</Transition>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
""
|
||||||
<LeftSideControls />
|
|
||||||
<div className="flex-1" />
|
|
||||||
<QualityDisplayAction />
|
|
||||||
<SeriesSelectionAction />
|
|
||||||
{/* <SourceSelectionControl media={props.media} />
|
|
||||||
<AirplayControl />
|
|
||||||
<ChromeCastControl /> */}
|
|
||||||
<FullscreenAction />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<Transition
|
||||||
</Transition>
|
animation="slide-down"
|
||||||
</BackdropAction>
|
show={show}
|
||||||
{props.children}
|
className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2"
|
||||||
</VideoPlayerError>
|
>
|
||||||
|
<HeaderAction
|
||||||
|
showControls={isMobile}
|
||||||
|
onClick={props.onGoBack}
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
<Transition
|
||||||
|
animation="slide-up"
|
||||||
|
show={show}
|
||||||
|
className={[
|
||||||
|
"pointer-events-auto absolute inset-x-0 bottom-0 flex flex-col px-4 pb-2",
|
||||||
|
props.includeSafeArea || isFullscreen
|
||||||
|
? "[margin-bottom:env(safe-area-inset-bottom)]"
|
||||||
|
: "",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<SeriesSelectionAction />
|
||||||
|
{/* <SourceSelectionControl media={props.media} /> */}
|
||||||
|
</div>
|
||||||
|
<FullscreenAction />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LeftSideControls />
|
||||||
|
<div className="flex-1" />
|
||||||
|
<QualityDisplayAction />
|
||||||
|
<SeriesSelectionAction />
|
||||||
|
{/* <SourceSelectionControl media={props.media} /> */}
|
||||||
|
<AirplayAction />
|
||||||
|
{/* <ChromeCastControl /> */}
|
||||||
|
<FullscreenAction />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</BackdropAction>
|
||||||
|
{props.children}
|
||||||
|
</VideoPlayerError>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</VideoPlayerBase>
|
</VideoPlayerBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,58 @@
|
||||||
import { WrapperRegisterInternal } from "@/video/components/internal/WrapperRegisterInternal";
|
import { WrapperRegisterInternal } from "@/video/components/internal/WrapperRegisterInternal";
|
||||||
|
import { VideoErrorBoundary } from "@/video/components/parts/VideoErrorBoundary";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { VideoPlayerContextProvider } from "../state/hooks";
|
import {
|
||||||
|
useVideoPlayerDescriptor,
|
||||||
|
VideoPlayerContextProvider,
|
||||||
|
} from "../state/hooks";
|
||||||
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
||||||
|
|
||||||
export interface VideoPlayerBaseProps {
|
export interface VideoPlayerBaseProps {
|
||||||
children?: React.ReactNode;
|
children?:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((data: { isFullscreen: boolean }) => React.ReactNode);
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
|
includeSafeArea?: boolean;
|
||||||
|
onGoBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayerBase(props: VideoPlayerBaseProps) {
|
function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
// TODO error boundary
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
// TODO move error boundary to only decorated, <VideoPlayer /> shouldn't have styling
|
const videoInterface = useInterface(descriptor);
|
||||||
|
const media = useMeta(descriptor);
|
||||||
|
|
||||||
|
const children =
|
||||||
|
typeof props.children === "function"
|
||||||
|
? props.children({ isFullscreen: videoInterface.isFullscreen })
|
||||||
|
: props.children;
|
||||||
|
|
||||||
|
// TODO move error boundary to only decorated, <VideoPlayer /> shouldn't have styling
|
||||||
return (
|
return (
|
||||||
<VideoPlayerContextProvider>
|
<VideoErrorBoundary onGoBack={props.onGoBack} media={media?.meta}>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
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]"
|
className={[
|
||||||
|
"is-video-player relative h-full w-full select-none overflow-hidden bg-black",
|
||||||
|
props.includeSafeArea || videoInterface.isFullscreen
|
||||||
|
? "[border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]"
|
||||||
|
: "",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<VideoElementInternal autoPlay={props.autoPlay} />
|
<VideoElementInternal autoPlay={props.autoPlay} />
|
||||||
<WrapperRegisterInternal wrapper={ref.current} />
|
<WrapperRegisterInternal wrapper={ref.current} />
|
||||||
<div className="absolute inset-0">{props.children}</div>
|
<div className="absolute inset-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</VideoErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoPlayerBase(props: VideoPlayerBaseProps) {
|
||||||
|
return (
|
||||||
|
<VideoPlayerContextProvider>
|
||||||
|
<VideoPlayerBaseWithState {...props} />
|
||||||
</VideoPlayerContextProvider>
|
</VideoPlayerContextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
30
src/video/components/actions/AirplayAction.tsx
Normal file
30
src/video/components/actions/AirplayAction.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AirplayAction(props: Props) {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const misc = useMisc(descriptor);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
controls.startAirplay();
|
||||||
|
}, [controls]);
|
||||||
|
|
||||||
|
if (!misc.canAirplay) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerIconButton
|
||||||
|
className={props.className}
|
||||||
|
onClick={handleClick}
|
||||||
|
icon={Icons.AIRPLAY}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
90
src/video/components/actions/KeyboardShortcutsAction.tsx
Normal file
90
src/video/components/actions/KeyboardShortcutsAction.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useInterface } from "@/video/state/logic/interface";
|
||||||
|
import { getPlayerState } from "@/video/state/cache";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
import { useProgress } from "@/video/state/logic/progress";
|
||||||
|
import { useVolumeControl } from "@/hooks/useVolumeToggle";
|
||||||
|
|
||||||
|
export function KeyboardShortcutsAction() {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const videoInterface = useInterface(descriptor);
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
const progress = useProgress(descriptor);
|
||||||
|
const { toggleVolume } = useVolumeControl(descriptor);
|
||||||
|
|
||||||
|
const curTime = useRef<number>(0);
|
||||||
|
useEffect(() => {
|
||||||
|
curTime.current = progress.time;
|
||||||
|
}, [progress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const el = state.wrapperElement;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
let isRolling = false;
|
||||||
|
const onKeyDown = (evt: KeyboardEvent) => {
|
||||||
|
if (!videoInterface.isFocused) return;
|
||||||
|
|
||||||
|
switch (evt.key.toLowerCase()) {
|
||||||
|
// Toggle fullscreen
|
||||||
|
case "f":
|
||||||
|
if (videoInterface.isFullscreen) {
|
||||||
|
controls.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
controls.enterFullscreen();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Skip backwards
|
||||||
|
case "arrowleft":
|
||||||
|
controls.setTime(curTime.current - 5);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Skip forward
|
||||||
|
case "arrowright":
|
||||||
|
controls.setTime(curTime.current + 5);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Pause / play
|
||||||
|
case " ":
|
||||||
|
if (mediaPlaying.isPaused) {
|
||||||
|
controls.play();
|
||||||
|
} else {
|
||||||
|
controls.pause();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Mute
|
||||||
|
case "m":
|
||||||
|
toggleVolume();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Do a barrel Roll!
|
||||||
|
case "r":
|
||||||
|
if (isRolling || evt.ctrlKey || evt.metaKey) return;
|
||||||
|
isRolling = true;
|
||||||
|
el.classList.add("roll");
|
||||||
|
setTimeout(() => {
|
||||||
|
isRolling = false;
|
||||||
|
el.classList.remove("roll");
|
||||||
|
}, 1000);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, [controls, descriptor, mediaPlaying, videoInterface, toggleVolume]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
import { useInitialized } from "@/video/components/hooks/useInitialized";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
@ -12,13 +13,15 @@ interface SourceControllerProps {
|
||||||
export function SourceController(props: SourceControllerProps) {
|
export function SourceController(props: SourceControllerProps) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
const { initialized } = useInitialized(descriptor);
|
||||||
const didInitialize = useRef<boolean>(false);
|
const didInitialize = useRef<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didInitialize.current) return;
|
if (didInitialize.current) return;
|
||||||
|
if (!initialized) return;
|
||||||
controls.setSource(props);
|
controls.setSource(props);
|
||||||
didInitialize.current = true;
|
didInitialize.current = true;
|
||||||
}, [props, controls]);
|
}, [props, controls, initialized]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
10
src/video/components/hooks/useInitialized.ts
Normal file
10
src/video/components/hooks/useInitialized.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function useInitialized(descriptor: string): { initialized: boolean } {
|
||||||
|
const misc = useMisc(descriptor);
|
||||||
|
const initialized = useMemo(() => !!misc.initalized, [misc]);
|
||||||
|
return {
|
||||||
|
initialized,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
||||||
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
|
@ -11,9 +12,13 @@ interface Props {
|
||||||
export function VideoElementInternal(props: Props) {
|
export function VideoElementInternal(props: Props) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const mediaPlaying = useMediaPlaying(descriptor);
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
const misc = useMisc(descriptor);
|
||||||
const ref = useRef<HTMLVideoElement>(null);
|
const ref = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
const initalized = useMemo(() => !!misc.wrapperInitialized, [misc]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!initalized) return;
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const provider = createVideoStateProvider(descriptor, ref.current);
|
const provider = createVideoStateProvider(descriptor, ref.current);
|
||||||
setProvider(descriptor, provider);
|
setProvider(descriptor, provider);
|
||||||
|
@ -22,9 +27,7 @@ export function VideoElementInternal(props: Props) {
|
||||||
unsetStateProvider(descriptor);
|
unsetStateProvider(descriptor);
|
||||||
destroy();
|
destroy();
|
||||||
};
|
};
|
||||||
}, [descriptor]);
|
}, [descriptor, initalized]);
|
||||||
|
|
||||||
// TODO shortcuts
|
|
||||||
|
|
||||||
// this element is remotely controlled by a state provider
|
// this element is remotely controlled by a state provider
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getPlayerState } from "@/video/state/cache";
|
import { getPlayerState } from "@/video/state/cache";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { updateMisc } from "@/video/state/logic/misc";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function WrapperRegisterInternal(props: {
|
export function WrapperRegisterInternal(props: {
|
||||||
|
@ -10,6 +11,7 @@ export function WrapperRegisterInternal(props: {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = getPlayerState(descriptor);
|
const state = getPlayerState(descriptor);
|
||||||
state.wrapperElement = props.wrapper;
|
state.wrapperElement = props.wrapper;
|
||||||
|
updateMisc(descriptor, state);
|
||||||
}, [props.wrapper, descriptor]);
|
}, [props.wrapper, descriptor]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
83
src/video/components/parts/VideoErrorBoundary.tsx
Normal file
83
src/video/components/parts/VideoErrorBoundary.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
|
import { Link } from "@/components/text/Link";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
import { Component, ReactNode } from "react";
|
||||||
|
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoErrorBoundaryProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
media?: MWMediaMeta;
|
||||||
|
onGoBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VideoErrorBoundary extends Component<
|
||||||
|
VideoErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
constructor(props: VideoErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError() {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: any, errorInfo: any) {
|
||||||
|
console.error("Render error caught", error, errorInfo);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const realError: Error = error as Error;
|
||||||
|
this.setState((s) => ({
|
||||||
|
...s,
|
||||||
|
hasError: true,
|
||||||
|
error: {
|
||||||
|
name: realError.name,
|
||||||
|
description: realError.message,
|
||||||
|
path: errorInfo.componentStack.split("\n")[1],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.hasError) return this.props.children;
|
||||||
|
|
||||||
|
// TODO make responsive, needs to work in tiny player
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 bg-denim-100">
|
||||||
|
<div className="pointer-events-auto absolute inset-x-0 top-0 flex flex-col py-6 px-8 pb-2">
|
||||||
|
<VideoPlayerHeader
|
||||||
|
media={this.props.media}
|
||||||
|
onClick={this.props.onGoBack}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage error={this.state.error} localSize>
|
||||||
|
The video player encounted a fatal error, please report it to the{" "}
|
||||||
|
<Link url={conf().DISCORD_LINK} newTab>
|
||||||
|
Discord server
|
||||||
|
</Link>{" "}
|
||||||
|
or on{" "}
|
||||||
|
<Link url={conf().GITHUB_LINK} newTab>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</ErrorMessage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useError } from "@/video/state/logic/error";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||||
|
@ -14,9 +15,9 @@ interface VideoPlayerErrorProps {
|
||||||
export function VideoPlayerError(props: VideoPlayerErrorProps) {
|
export function VideoPlayerError(props: VideoPlayerErrorProps) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
// TODO add error state
|
const errorData = useError(descriptor);
|
||||||
|
|
||||||
const err = null as any;
|
const err = errorData.error;
|
||||||
|
|
||||||
if (!err) return props.children as any;
|
if (!err) return props.children as any;
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ export function VideoPlayerError(props: VideoPlayerErrorProps) {
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-denim-100">
|
<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" />
|
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||||
<Title>Failed to load media</Title>
|
<Title>Failed to load media</Title>
|
||||||
<p className="my-6 max-w-lg">
|
<p className="my-6 max-w-lg text-center">
|
||||||
{err?.name}: {err?.description}
|
{err?.name}: {err?.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getIfBookmarkedFromPortable,
|
getIfBookmarkedFromPortable,
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
} from "@/state/bookmark";
|
} from "@/state/bookmark";
|
||||||
|
import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
||||||
|
|
||||||
interface VideoPlayerHeaderProps {
|
interface VideoPlayerHeaderProps {
|
||||||
media?: MWMediaMeta;
|
media?: MWMediaMeta;
|
||||||
|
@ -53,11 +54,10 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{props.showControls ? null : (
|
{props.showControls ? (
|
||||||
// <>
|
<AirplayAction />
|
||||||
// <AirplayControl />
|
) : (
|
||||||
// <ChromeCastControl />
|
// chromecontrol
|
||||||
// </>
|
|
||||||
<BrandPill />
|
<BrandPill />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,9 @@ export type VideoPlayerEvent =
|
||||||
| "source"
|
| "source"
|
||||||
| "progress"
|
| "progress"
|
||||||
| "interface"
|
| "interface"
|
||||||
| "meta";
|
| "meta"
|
||||||
|
| "error"
|
||||||
|
| "misc";
|
||||||
|
|
||||||
function createEventString(id: string, event: VideoPlayerEvent): string {
|
function createEventString(id: string, event: VideoPlayerEvent): string {
|
||||||
return `_vid:::${id}:::${event}`;
|
return `_vid:::${id}:::${event}`;
|
||||||
|
|
|
@ -29,11 +29,12 @@ function initPlayer(): VideoPlayerState {
|
||||||
|
|
||||||
meta: null,
|
meta: null,
|
||||||
source: null,
|
source: null,
|
||||||
|
|
||||||
error: null,
|
error: null,
|
||||||
|
canAirplay: false,
|
||||||
|
initalized: false,
|
||||||
|
|
||||||
pausedWhenSeeking: false,
|
pausedWhenSeeking: false,
|
||||||
canAirplay: false,
|
|
||||||
|
|
||||||
stateProvider: null,
|
stateProvider: null,
|
||||||
wrapperElement: null,
|
wrapperElement: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,9 @@ export function useControls(
|
||||||
setVolume(volume) {
|
setVolume(volume) {
|
||||||
state.stateProvider?.setVolume(volume);
|
state.stateProvider?.setVolume(volume);
|
||||||
},
|
},
|
||||||
|
startAirplay() {
|
||||||
|
state.stateProvider?.startAirplay();
|
||||||
|
},
|
||||||
|
|
||||||
// other controls
|
// other controls
|
||||||
setLeftControlsHover(hovering) {
|
setLeftControlsHover(hovering) {
|
||||||
|
|
38
src/video/state/logic/error.ts
Normal file
38
src/video/state/logic/error.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||||
|
import { VideoPlayerState } from "../types";
|
||||||
|
|
||||||
|
export type VideoErrorEvent = {
|
||||||
|
error: null | {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getErrorFromState(state: VideoPlayerState): VideoErrorEvent {
|
||||||
|
return {
|
||||||
|
error: state.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateError(descriptor: string, state: VideoPlayerState) {
|
||||||
|
sendEvent<VideoErrorEvent>(descriptor, "error", getErrorFromState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useError(descriptor: string): VideoErrorEvent {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const [data, setData] = useState<VideoErrorEvent>(getErrorFromState(state));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(payload: CustomEvent<VideoErrorEvent>) {
|
||||||
|
setData(payload.detail);
|
||||||
|
}
|
||||||
|
listenEvent(descriptor, "error", update);
|
||||||
|
return () => {
|
||||||
|
unlistenEvent(descriptor, "error", update);
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
39
src/video/state/logic/misc.ts
Normal file
39
src/video/state/logic/misc.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getPlayerState } from "../cache";
|
||||||
|
import { listenEvent, sendEvent, unlistenEvent } from "../events";
|
||||||
|
import { VideoPlayerState } from "../types";
|
||||||
|
|
||||||
|
export type VideoMiscError = {
|
||||||
|
canAirplay: boolean;
|
||||||
|
wrapperInitialized: boolean;
|
||||||
|
initalized: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMiscFromState(state: VideoPlayerState): VideoMiscError {
|
||||||
|
return {
|
||||||
|
canAirplay: state.canAirplay,
|
||||||
|
wrapperInitialized: !!state.wrapperElement,
|
||||||
|
initalized: state.initalized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMisc(descriptor: string, state: VideoPlayerState) {
|
||||||
|
sendEvent<VideoMiscError>(descriptor, "misc", getMiscFromState(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMisc(descriptor: string): VideoMiscError {
|
||||||
|
const state = getPlayerState(descriptor);
|
||||||
|
const [data, setData] = useState<VideoMiscError>(getMiscFromState(state));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(payload: CustomEvent<VideoMiscError>) {
|
||||||
|
setData(payload.detail);
|
||||||
|
}
|
||||||
|
listenEvent(descriptor, "misc", update);
|
||||||
|
return () => {
|
||||||
|
unlistenEvent(descriptor, "misc", update);
|
||||||
|
};
|
||||||
|
}, [descriptor]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ export type VideoPlayerStateController = {
|
||||||
exitFullscreen(): void;
|
exitFullscreen(): void;
|
||||||
enterFullscreen(): void;
|
enterFullscreen(): void;
|
||||||
setVolume(volume: number): void;
|
setVolume(volume: number): void;
|
||||||
|
startAirplay(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { updateMisc } from "@/video/state/logic/misc";
|
||||||
import { getPlayerState } from "../cache";
|
import { getPlayerState } from "../cache";
|
||||||
import { VideoPlayerStateProvider } from "./providerTypes";
|
import { VideoPlayerStateProvider } from "./providerTypes";
|
||||||
|
|
||||||
|
@ -7,6 +8,8 @@ export function setProvider(
|
||||||
) {
|
) {
|
||||||
const state = getPlayerState(descriptor);
|
const state = getPlayerState(descriptor);
|
||||||
state.stateProvider = provider;
|
state.stateProvider = provider;
|
||||||
|
state.initalized = true;
|
||||||
|
updateMisc(descriptor, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,12 +13,44 @@ import {
|
||||||
getStoredVolume,
|
getStoredVolume,
|
||||||
setStoredVolume,
|
setStoredVolume,
|
||||||
} from "@/video/components/hooks/volumeStore";
|
} from "@/video/components/hooks/volumeStore";
|
||||||
|
import { updateError } from "@/video/state/logic/error";
|
||||||
|
import { updateMisc } from "@/video/state/logic/misc";
|
||||||
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";
|
||||||
import { updateProgress } from "../logic/progress";
|
import { updateProgress } from "../logic/progress";
|
||||||
import { handleBuffered } from "./utils";
|
import { handleBuffered } from "./utils";
|
||||||
|
|
||||||
|
function errorMessage(err: MediaError) {
|
||||||
|
switch (err.code) {
|
||||||
|
case MediaError.MEDIA_ERR_ABORTED:
|
||||||
|
return {
|
||||||
|
code: "ABORTED",
|
||||||
|
description: "Video was aborted",
|
||||||
|
};
|
||||||
|
case MediaError.MEDIA_ERR_NETWORK:
|
||||||
|
return {
|
||||||
|
code: "NETWORK_ERROR",
|
||||||
|
description: "A network error occured, the video failed to stream",
|
||||||
|
};
|
||||||
|
case MediaError.MEDIA_ERR_DECODE:
|
||||||
|
return {
|
||||||
|
code: "DECODE_ERROR",
|
||||||
|
description: "Video stream could not be decoded",
|
||||||
|
};
|
||||||
|
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||||
|
return {
|
||||||
|
code: "SRC_NOT_SUPPORTED",
|
||||||
|
description: "The video type is not supported by your browser",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
code: "UNKNOWN_ERROR",
|
||||||
|
description: "Unknown media error occured",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createVideoStateProvider(
|
export function createVideoStateProvider(
|
||||||
descriptor: string,
|
descriptor: string,
|
||||||
playerEl: HTMLVideoElement
|
playerEl: HTMLVideoElement
|
||||||
|
@ -48,6 +80,11 @@ export function createVideoStateProvider(
|
||||||
(player as any).webkitEnterFullscreen();
|
(player as any).webkitEnterFullscreen();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
startAirplay() {
|
||||||
|
const videoPlayer = player as any;
|
||||||
|
if (videoPlayer.webkitShowPlaybackTargetPicker)
|
||||||
|
videoPlayer.webkitShowPlaybackTargetPicker();
|
||||||
|
},
|
||||||
setTime(t) {
|
setTime(t) {
|
||||||
// clamp time between 0 and max duration
|
// clamp time between 0 and max duration
|
||||||
let time = Math.min(t, player.duration);
|
let time = Math.min(t, player.duration);
|
||||||
|
@ -103,7 +140,7 @@ export function createVideoStateProvider(
|
||||||
name: `Not supported`,
|
name: `Not supported`,
|
||||||
description: "Your browser does not support HLS video",
|
description: "Your browser does not support HLS video",
|
||||||
};
|
};
|
||||||
// TODO dispatch error
|
updateError(descriptor, state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +152,7 @@ export function createVideoStateProvider(
|
||||||
name: `error ${data.details}`,
|
name: `error ${data.details}`,
|
||||||
description: data.error?.message ?? "Something went wrong",
|
description: data.error?.message ?? "Something went wrong",
|
||||||
};
|
};
|
||||||
// TODO dispatch error
|
updateError(descriptor, state);
|
||||||
}
|
}
|
||||||
console.error("HLS error", data);
|
console.error("HLS error", data);
|
||||||
});
|
});
|
||||||
|
@ -199,18 +236,22 @@ export function createVideoStateProvider(
|
||||||
const canAirplay = (e: any) => {
|
const canAirplay = (e: any) => {
|
||||||
if (e.availability === "available") {
|
if (e.availability === "available") {
|
||||||
state.canAirplay = true;
|
state.canAirplay = true;
|
||||||
// TODO dispatch airplay
|
updateMisc(descriptor, state);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const error = () => {
|
const error = () => {
|
||||||
console.error("Native video player threw error", player.error);
|
if (player.error) {
|
||||||
state.error = player.error
|
const err = errorMessage(player.error);
|
||||||
? {
|
console.error("Native video player threw error", player.error);
|
||||||
description: player.error.message,
|
state.error = {
|
||||||
name: `Error ${player.error.code}`,
|
description: err.description,
|
||||||
}
|
name: `Error ${err.code}`,
|
||||||
: null;
|
};
|
||||||
// TODO dispatch error
|
this.pause(); // stop video from playing
|
||||||
|
} else {
|
||||||
|
state.error = null;
|
||||||
|
}
|
||||||
|
updateError(descriptor, state);
|
||||||
};
|
};
|
||||||
|
|
||||||
state.wrapperElement?.addEventListener("click", isFocused);
|
state.wrapperElement?.addEventListener("click", isFocused);
|
||||||
|
|
|
@ -50,16 +50,17 @@ export type VideoPlayerState = {
|
||||||
url: string;
|
url: string;
|
||||||
type: MWStreamType;
|
type: MWStreamType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// misc
|
||||||
|
canAirplay: boolean;
|
||||||
|
initalized: boolean;
|
||||||
error: null | {
|
error: null | {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// misc
|
|
||||||
pausedWhenSeeking: boolean; // when seeking, used to store if paused when started to seek
|
|
||||||
canAirplay: boolean;
|
|
||||||
|
|
||||||
// backing fields
|
// backing fields
|
||||||
|
pausedWhenSeeking: boolean; // when seeking, used to store if paused when started to seek
|
||||||
stateProvider: VideoPlayerStateProvider | null;
|
stateProvider: VideoPlayerStateProvider | null;
|
||||||
wrapperElement: HTMLDivElement | null;
|
wrapperElement: HTMLDivElement | null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -131,7 +131,7 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<html data-full="true" />
|
<html data-full="true" />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<VideoPlayer autoPlay onGoBack={goBack}>
|
<VideoPlayer includeSafeArea autoPlay onGoBack={goBack}>
|
||||||
<MetaController data={metaProps} seasonData={metaSeasonData} />
|
<MetaController data={metaProps} seasonData={metaSeasonData} />
|
||||||
<SourceController
|
<SourceController
|
||||||
source={props.stream.streamUrl}
|
source={props.stream.streamUrl}
|
||||||
|
|
Loading…
Reference in a new issue