/** * Copyright (C) 2019-2021 Carl Kittelberger * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import * as React from 'react'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { Breadcrumb, Button, ButtonGroup, Col, ResponsiveEmbed, Row, Tab, Tabs, } from 'react-bootstrap'; import { useIntl } from 'react-intl'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { GetServerSideProps, GetServerSidePropsResult, InferGetServerSidePropsType } from 'next'; import { VideoEntry } from 'util/datatypes/VideoList'; import DownloadButton from 'components/DownloadButton'; import { basename } from 'path'; import withSession from 'util/session'; import { getDASHManifestURL, getHLSMasterURL } from '../../util'; import VideoPlayer from '../../components/VideoPlayer'; import CopyField from '../../components/CopyField'; import { FormattedMessage } from '../../components/localization'; import sanitizeFileName from '../../util/sanitizeFileName'; import sanitizeTitle from '../../util/sanitizeTitle'; import { notFound } from '../../util/status'; import { getDownloadURL, getIndex, getVideos, submitPreferences, } from '../../util/api'; interface VideoPlayerPageParameters { id: string, vslug: string, } interface VideoPlayerPageProps { id?: string, vslug?: string, video?: number, volume?: number, redirect?: boolean, title?: string, hlsServerURL?: string, dashServerURL?: string, basePath?: string, twitchPlayerParentKey?: string, } const getProps = withSession(async (req, _res, { id, vslug }: VideoPlayerPageParameters): Promise> => { if (typeof id !== 'string') { throw new Error('only expected a single id'); } if (typeof vslug !== 'string') { throw new Error('only expected a single vslug'); } // Fetch URL to thumbnails server const { ids, servers: { hls: hlsServerURL, dash: dashServerURL }, } = await getIndex(); const vodMeta = ids.find(({ id: thisID, }: { id: string }) => id === thisID); if (!vodMeta) { return { props: {} }; } // Fetch list of videos for this VOD ID const vodInfo = await getVideos(id); let videos; if (Array.isArray(vodInfo)) { videos = vodInfo; } else { videos = vodInfo.videos; } // Check if vslug is actually an index number (old app style) /* NOTE - parseInt will accept strings CONTAINING numbers. This is supposed to reject strings that are not JUST numbers. */ const vindexNum = +vslug; if (!Number.isNaN(vindexNum)) { // Check if video exists if (vindexNum < 0 || vindexNum >= videos.length) { return { props: {} }; } const video = videos[vindexNum]; return { props: { redirect: true, id, video, vslug: video.slug, }, }; } // Check if vslug is actually point to a file name const sanitizedFileName = `${sanitizeFileName(basename(vslug, '.mp4'))}.mp4`; const realVIndex = videos.findIndex( (video: VideoEntry) => video.fileName === sanitizedFileName, ); if (realVIndex >= 0) { const video = videos[realVIndex]; return { props: { redirect: true, id, video: realVIndex, vslug: video.slug, }, }; } // Check if we can find any video with matching vslug const video = videos.find(({ slug }: VideoEntry) => slug === vslug); if (!video) { return { props: {} }; } // At this point we found the right video, just get more information at this point const { title } = vodMeta; // let basePath = null; // basePath = `${JSON.stringify(req.toString())}`; // TODO - detect HTTPS properly // NOTE - req.socket.encrypted fails in TypeScript due to typing const basePath = `https://${req.headers.host}`; // ask for user's volume preference let volume: number = 0.5; const volumeSessionValue = req.session.get('volume'); if (typeof volumeSessionValue === 'string') { const parsedNumber = parseFloat(volumeSessionValue); if (!Number.isNaN(parsedNumber)) { volume = parsedNumber; } } else if (typeof volumeSessionValue === 'number' && !Number.isNaN(volumeSessionValue)) { volume = volumeSessionValue; } if (!req.headers.host) { throw new Error(JSON.stringify(req.headers)) } // Pass data to the page via props return { props: { id, vslug, video, volume, title, hlsServerURL, dashServerURL, twitchPlayerParentKey: req.headers.host.split(':')[0], basePath, }, }; }); export const getServerSideProps: GetServerSideProps = async ({ req, res, params, }) => getProps(req, res, params); export default function VideoPlayerPage({ id, vslug, video, volume, // extra, redirect, title, hlsServerURL, dashServerURL, basePath, twitchPlayerParentKey, }: InferGetServerSidePropsType) { if (redirect) { const router = useRouter(); React.useEffect(() => { router.push(`/${id}/${vslug}`); }); return (

You will be redirected {' '} here

); } if (!video) { return notFound(); } const intl = useIntl(); const playerRef = React.useRef(); const { fileName, title: videoTitle, sourceVideoURL, sourceVideoStart, } = video; let volumeChangeDebounceTimer: NodeJS.Timeout = null; const [currentVolume, trackCurrentVolume] = React.useState(volume); async function onVolumeChange() { const newVolume = playerRef.current.player.volume(); if (Math.abs(newVolume - currentVolume) > 0.05) { if (volumeChangeDebounceTimer !== null) { clearTimeout(volumeChangeDebounceTimer); } volumeChangeDebounceTimer = setTimeout(() => { submitPreferences({ volume: newVolume, }); trackCurrentVolume(newVolume); }, 1000); } } const twitchEmbedURL = new URL("https://player.twitch.tv"); twitchEmbedURL.searchParams.append('video', sourceVideoURL.split('/').pop()); twitchEmbedURL.searchParams.append('parent', twitchPlayerParentKey); twitchEmbedURL.searchParams.append('t', `${Math.floor(sourceVideoStart / 60)}m${sourceVideoStart % 60}s`) return (
{videoTitle} {' '} – {' '} {title} {' '} – {' '} {intl.formatMessage({ id: 'App.title', description: 'The full title of the website', defaultMessage: 'Games Done Quick Instant Archive', })} {title} {videoTitle}

{title} : {' '} {videoTitle}

{/* */} { sourceVideoURL ? ( ) : ( '' ) } {[basePath, id, vslug].join('/')}
); }