252 lines
9.2 KiB
TypeScript
252 lines
9.2 KiB
TypeScript
import React from "react";
|
|
import Image from "next/image";
|
|
import PageBase from "./shared/PageBase";
|
|
import { VideoMetadata } from "./shared/database.d";
|
|
import { formatDate } from "./shared/format";
|
|
import ChatReplayPanel from "./shared/ChatReplay/ChatReplayPanel";
|
|
import VideoCard from "./shared/VideoCard";
|
|
import Link from "next/link";
|
|
import ServiceUnavailablePage from "./ServiceUnavailablePage";
|
|
import VideoActionButtons from "./shared/VideoActionButtons";
|
|
import { useWindowSize } from "./shared/hooks/useWindowSize";
|
|
import MemoLinkify from "./shared/MemoLinkify";
|
|
import VideoPlayerHead from "./shared/VideoPlayerHead";
|
|
import ClientRender from "./shared/ClientRender";
|
|
import VideoPlayer2 from "./shared/VideoPlayer/VideoPlayer2";
|
|
import ExpandableContainer from "./ExpandableContainer";
|
|
import CommentSection from "./CommentSection";
|
|
|
|
const format = (n: number) => Intl.NumberFormat("en-US").format(n);
|
|
|
|
export type WatchPageProps = {
|
|
videoInfo: VideoMetadata;
|
|
hasChat: boolean;
|
|
relatedVideos: VideoMetadata[];
|
|
channelVideoCount: number;
|
|
channelProfileURL: string;
|
|
};
|
|
|
|
const getFile = (videoInfo: VideoMetadata, suffix: string) =>
|
|
videoInfo.files.find((file) => file.name.includes(suffix))?.url;
|
|
|
|
const WatchPage = (props: WatchPageProps) => {
|
|
if (!props.videoInfo) return <ServiceUnavailablePage />;
|
|
|
|
const { videoInfo, hasChat, relatedVideos } = props;
|
|
|
|
const [isChatVisible, setIsChatVisible] = React.useState(false);
|
|
const refMobileScrollTarget = React.useRef<HTMLDivElement>(null);
|
|
const { innerWidth, innerHeight } = useWindowSize();
|
|
|
|
React.useEffect(() => {
|
|
if (isChatVisible)
|
|
refMobileScrollTarget.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [isChatVisible]);
|
|
|
|
React.useEffect(() => {
|
|
if (!videoInfo || window.location.host.startsWith("localhost")) return;
|
|
fetch(
|
|
"/api/pv?channel_id=" +
|
|
videoInfo.channel_id +
|
|
"&video_id=" +
|
|
videoInfo.video_id
|
|
);
|
|
}, [videoInfo]);
|
|
|
|
const [playbackProgress, setPlaybackProgress] = React.useState(0);
|
|
/*const [fmtVideo, fmtAudio] = videoInfo.format_id.split("+");
|
|
const urlVideo = getFile(videoInfo, ".f" + fmtVideo);
|
|
const urlAudio = getFile(videoInfo, ".f" + fmtAudio);*/
|
|
const urlVideo = getFile(videoInfo, ".mkv");
|
|
const urlAudio = '';
|
|
// srcAudio={urlAudio}
|
|
|
|
const urlThumb = getFile(videoInfo, ".webp") || getFile(videoInfo, ".jpg");
|
|
const urlChat = getFile(videoInfo, ".chat.json");
|
|
const urlInfo = getFile(videoInfo, ".info.json");
|
|
|
|
// Function to check if the user agent is Firefox
|
|
const isFirefox = typeof window !== 'undefined' && navigator.userAgent.includes('Firefox');
|
|
|
|
// Update the video URL if it's Firefox and the URL ends with .mkv
|
|
let updatedVideoUrl = urlVideo;
|
|
|
|
if (isFirefox && urlVideo.endsWith('.mkv')) {
|
|
// For Firefox, rewrite to .webm and prepend /convert/
|
|
updatedVideoUrl = urlVideo.replace('/vpath/', '/convert/vpath/').replace('.mkv', '.webm');
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
if (typeof window !== 'undefined') {
|
|
console.log('User Agent:', navigator.userAgent);
|
|
console.log('Is Firefox:', isFirefox);
|
|
console.log('Updated Video URL because Firefox beeing special (https://bugzilla.mozilla.org/show_bug.cgi?id=1422891):', updatedVideoUrl);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<PageBase>
|
|
<VideoPlayerHead videoInfo={videoInfo} />
|
|
<div
|
|
className={["flex lg:flex-row flex-col lg:h-auto"].join(" ")}
|
|
style={{
|
|
height: isChatVisible && innerWidth < 640 ? innerHeight : "auto",
|
|
}}
|
|
>
|
|
<div className="w-full lg:w-3/4">
|
|
<div className="lg:absolute lg:top-0" ref={refMobileScrollTarget} />
|
|
<div
|
|
className="relative bg-gray-400 h-0"
|
|
style={{ paddingBottom: "56.25%" }}
|
|
>
|
|
<div className="absolute inset-0 w-full h-full">
|
|
<VideoPlayer2
|
|
key={updatedVideoUrl}
|
|
videoId={videoInfo.id}
|
|
srcVideo={updatedVideoUrl}
|
|
srcPoster={urlThumb}
|
|
captions={videoInfo.files
|
|
.filter((file) => file.name.endsWith(".ytt"))
|
|
.map(({ name }) => {
|
|
const lang = name.split(".")[1];
|
|
return {
|
|
lang,
|
|
src: getFile(videoInfo, name),
|
|
};
|
|
})}
|
|
onPlaybackProgress={setPlaybackProgress}
|
|
autoplay
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={[
|
|
"w-full lg:w-1/4 lg:pl-4",
|
|
isChatVisible ? "flex-1" : "",
|
|
].join(" ")}
|
|
>
|
|
{!hasChat ? (
|
|
<div className="border border-gray-800 rounded p-4 text-center">
|
|
<p>Chat replay unavailable</p>
|
|
</div>
|
|
) : (
|
|
<ChatReplayPanel
|
|
src={urlChat}
|
|
currentTimeSeconds={playbackProgress}
|
|
onChatToggle={setIsChatVisible}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex lg:flex-row flex-col">
|
|
<div className="w-full lg:w-3/4">
|
|
<div className="mt-4 mx-6">
|
|
<h1 className="text-2xl mb-2">{videoInfo.title}</h1>
|
|
<div className="flex flex-row justify-between">
|
|
<ClientRender enableSSR>
|
|
<p className="text-gray-400">
|
|
{format(videoInfo.view_count)} views ·{" "}
|
|
<span
|
|
title={
|
|
videoInfo.timestamps?.publishedAt
|
|
? new Date(
|
|
videoInfo.timestamps?.publishedAt
|
|
).toLocaleString()
|
|
: "(exact timestamp unknown)"
|
|
}
|
|
>
|
|
Uploaded{" "}
|
|
{formatDate(
|
|
new Date(
|
|
videoInfo.timestamps?.publishedAt ||
|
|
videoInfo.upload_date
|
|
)
|
|
)}
|
|
</span>{" "}
|
|
·{" "}
|
|
<span
|
|
title={new Date(
|
|
videoInfo.archived_timestamp +
|
|
(videoInfo.archived_timestamp.endsWith("Z") ? "" : "Z")
|
|
).toLocaleString()}
|
|
>
|
|
Archived{" "}
|
|
{formatDate(
|
|
new Date(
|
|
videoInfo.archived_timestamp +
|
|
(videoInfo.archived_timestamp.endsWith("Z")
|
|
? ""
|
|
: "Z")
|
|
)
|
|
)}
|
|
</span>
|
|
</p>
|
|
</ClientRender>
|
|
<div>
|
|
<span className="text-green-500">
|
|
{format(videoInfo.like_count)} likes
|
|
</span>{" "}
|
|
/{" "}
|
|
<span className="text-red-500">
|
|
{format(videoInfo.dislike_count)} dislikes
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-row mt-2">
|
|
<VideoActionButtons full video={videoInfo} />
|
|
</div>
|
|
<div className="mt-4 pb-4 border-b border-gray-900">
|
|
<div className="inline-block">
|
|
<Link href={"/channel/" + videoInfo.channel_id}>
|
|
<a className="mb-4 mb-4 hover:underline flex flex-row">
|
|
<div className="w-12 h-12 rounded-full overflow-hidden relative">
|
|
<Image
|
|
priority
|
|
alt="Channel thumbnail"
|
|
src={"https://i.imgur.com/rBvryFM.png"||props.channelProfileURL}
|
|
layout="fixed"
|
|
width={48}
|
|
height={48}
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="font-bold text-lg leading-tight">
|
|
{videoInfo.channel_name}
|
|
</p>
|
|
<span className="text-gray-400 leading-tight">
|
|
{props.channelVideoCount} videos
|
|
</span>
|
|
</div>
|
|
</a>
|
|
</Link>
|
|
</div>
|
|
<ExpandableContainer>
|
|
<div className="whitespace-pre-line break-words text-gray-300">
|
|
<MemoLinkify linkClassName="text-blue-300 hover:underline focus:outline-none focus:underline">
|
|
{videoInfo.description}
|
|
</MemoLinkify>
|
|
</div>
|
|
</ExpandableContainer>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="w-full lg:w-1/4 lg:pl-4">
|
|
<div className="mt-6">
|
|
<h2 className="text-xl font-bold mb-2">Related videos</h2>
|
|
<div>
|
|
{relatedVideos.map((video) => (
|
|
<div className="mb-4" key={video.video_id}>
|
|
<VideoCard small video={video} key={video.video_id} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageBase>
|
|
);
|
|
};
|
|
|
|
export default WatchPage;
|