Files
mtdb_movie/resources/client/videos/site-video-player.tsx
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

182 lines
5.3 KiB
TypeScript
Executable File

import {guessPlayerProvider} from '@common/player/utils/guess-player-provider';
import {VideoPlayer} from '@common/player/ui/video-player/video-player';
import {VideoThumbnail} from '@app/videos/video-thumbnail';
import {IconButton} from '@common/ui/buttons/icon-button';
import {MediaPlayIcon} from '@common/icons/media/media-play';
import React, {memo, useEffect, useRef} from 'react';
import {Video} from '@app/titles/models/video';
import {Title} from '@app/titles/models/title';
import {Episode} from '@app/titles/models/episode';
import {VideoPlayerSkeleton} from '@app/videos/video-player-skeleton';
import {MediaItem} from '@common/player/media-item';
import {useLogVideoPlay} from '@app/videos/requests/use-log-video-play';
import {PlayerActions} from '@common/player/hooks/use-player-actions';
import {getWatchLink} from '@app/videos/watch-page/get-watch-link';
import {useNavigate} from '@common/utils/hooks/use-navigate';
import {isSameMedia} from '@common/player/utils/is-same-media';
import {Trans} from '@common/i18n/trans';
import {EpisodeSelector} from '@app/videos/watch-page/episode-selector';
interface Props {
video: Video;
relatedVideos?: Video[];
autoPlay?: boolean;
title?: Title;
episode?: Episode;
mediaItemId?: string;
logPlays?: boolean;
showEpisodeSelector?: boolean;
}
export const SiteVideoPlayer = memo((props: Props) => {
const {video, autoPlay, title, episode} = props;
if (
video.type === 'video' ||
video.type === 'stream' ||
(video.type === 'embed' && video.src.includes('youtube'))
) {
return <NativeVideoPlayer {...props} />;
}
if (video.type === 'embed') {
return <EmbedPlayer src={video.src} autoPlay={autoPlay} />;
}
if (video.type === 'external') {
return (
<div className="relative">
<VideoThumbnail
title={title}
episode={episode}
video={video}
fallback={<div className="aspect-video w-full bg-fg-base/4" />}
/>
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center"
onClick={() => window.open(video.src, '_blank')}
>
<IconButton variant="flat" color="primary" size="lg">
<MediaPlayIcon />
</IconButton>
</div>
</div>
);
}
return <VideoPlayerSkeleton />;
});
interface EmberPlayerProps {
src: string;
autoPlay?: boolean;
}
const EmbedPlayer = memo(({src, autoPlay}: EmberPlayerProps) => {
let finalSrc = '';
try {
const url = src.includes('<iframe') ? src.match(/src="([^"]*)"/)?.[1] : src;
const parsed = new URL(url || '');
parsed.searchParams.set('autoplay', autoPlay ? '1' : '0');
finalSrc = parsed.toString();
} catch {}
if (!finalSrc) {
return (
<div className="flex aspect-video w-full items-center justify-center">
<div className="rounded-panel border p-10">
<Trans message="There was an issue playting this video." />
</div>
</div>
);
}
return (
<iframe
src={finalSrc}
className="aspect-video w-full"
allowFullScreen
allow="autoplay; encrypted-media; picture-in-picture;"
/>
);
});
function NativeVideoPlayer({
video,
title,
episode,
mediaItemId,
relatedVideos,
autoPlay,
logPlays,
showEpisodeSelector,
}: Props) {
const playerRef = useRef<PlayerActions>(null!);
const logVideoPlay = useLogVideoPlay(playerRef, {enabled: logPlays});
const mediaItem = videoToMediaItem(video, mediaItemId);
const related = relatedVideos?.map(v => videoToMediaItem(v)) ?? [];
const navigate = useNavigate();
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
logVideoPlay();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () =>
document.removeEventListener('visibilitychange', handleVisibilityChange);
}, [logVideoPlay]);
return (
<VideoPlayer
apiRef={playerRef}
id="player"
queue={[mediaItem, ...related]}
autoPlay={autoPlay}
onBeforePlayNext={nextMedia => {
if (nextMedia && !isSameMedia(mediaItem, nextMedia)) {
navigate(getWatchLink(nextMedia.meta));
}
return true;
}}
onDestroy={() => logVideoPlay()}
listeners={{
playbackEnd: () => logVideoPlay(),
beforeCued: ({previous}) => {
// only log when cueing from previous video and not when cueing initial one
if (previous) {
logVideoPlay();
}
},
}}
rightActions={
showEpisodeSelector && title && episode ? (
<EpisodeSelector
title={title}
currentEpisode={episode}
onSelected={episode => {
navigate(getWatchLink(episode.primary_video));
}}
/>
) : undefined
}
/>
);
}
function videoToMediaItem(video: Video, mediaItemId?: string): MediaItem {
return {
id: mediaItemId || video.id,
provider: guessPlayerProvider(video.src),
src: video.src,
poster: video.thumbnail,
meta: video,
initialTime: video.latest_play?.time_watched ?? undefined,
captions: video.captions?.map(caption => ({
id: caption.id,
src: caption.url,
label: caption.name,
language: caption.language,
})),
};
}