diff --git a/README.md b/README.md index bc9c1c9..764c52e 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ currently only supports a ``--debug``` flag + +WatchpPage.tsx is a customized version of https://github.com/ragtag-archive/archive-browser/blob/master/modules/WatchPage.tsx to support webm delivery for Firefox Users - is wonky but works (kinda). diff --git a/WatchPage.tsx b/WatchPage.tsx new file mode 100644 index 0000000..f2cb43a --- /dev/null +++ b/WatchPage.tsx @@ -0,0 +1,252 @@ +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;