181
resources/client/videos/site-video-player.tsx
Executable file
181
resources/client/videos/site-video-player.tsx
Executable file
@@ -0,0 +1,181 @@
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user