| 
									
										
										
										
											2021-01-05 15:25:09 +00:00
										 |  |  |  | /** | 
					
						
							|  |  |  |  |  * Copyright (C) 2019-2021 Carl Kittelberger <icedream@icedream.pw> | 
					
						
							|  |  |  |  |  * | 
					
						
							|  |  |  |  |  * 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 <http://www.gnu.org/licenses/>. | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | import * as React from 'react'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | import Head from 'next/head'; | 
					
						
							|  |  |  |  | import Link from 'next/link'; | 
					
						
							|  |  |  |  | import { useRouter } from 'next/router'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | import { | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   Breadcrumb, Button, ButtonGroup, Col, Ratio, Row, Tab, Tabs, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | } from 'react-bootstrap'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | import { useIntl } from 'react-intl'; | 
					
						
							|  |  |  |  | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | 
					
						
							| 
									
										
										
										
											2021-01-04 16:31:36 +00:00
										 |  |  |  | import { GetServerSideProps, GetServerSidePropsResult, InferGetServerSidePropsType } from 'next'; | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | import { VideoEntry } from 'util/datatypes/VideoList'; | 
					
						
							| 
									
										
										
										
											2021-01-03 17:42:37 +00:00
										 |  |  |  | import { basename } from 'path'; | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  | import withSession from 'util/session'; | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  | import { ParsedUrlQuery } from 'querystring'; | 
					
						
							|  |  |  |  | import YouTubePlayer from 'react-youtube'; | 
					
						
							| 
									
										
										
										
											2023-01-09 03:02:23 +00:00
										 |  |  |  | import { faTwitch, faYoutube } from '@fortawesome/free-brands-svg-icons'; | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  | import { getDASHManifestURL, getHLSMasterURL } from '../../util'; | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | import VideoPlayer from '../../components/VideoPlayer'; | 
					
						
							|  |  |  |  | import CopyField from '../../components/CopyField'; | 
					
						
							|  |  |  |  | import { FormattedMessage } from '../../components/localization'; | 
					
						
							|  |  |  |  | import sanitizeFileName from '../../util/sanitizeFileName'; | 
					
						
							|  |  |  |  | import { notFound } from '../../util/status'; | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  | import { | 
					
						
							|  |  |  |  |   getDownloadURL, | 
					
						
							|  |  |  |  |   getIndex, | 
					
						
							|  |  |  |  |   getVideos, | 
					
						
							|  |  |  |  |   submitPreferences, | 
					
						
							|  |  |  |  | } from '../../util/api'; | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  | import DownloadButton from '../../components/DownloadButton'; | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  | interface VideoPlayerPageParameters extends ParsedUrlQuery { | 
					
						
							| 
									
										
										
										
											2021-01-04 16:31:36 +00:00
										 |  |  |  |   id: string, | 
					
						
							|  |  |  |  |   vslug: string, | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | interface VideoPlayerPageProps { | 
					
						
							|  |  |  |  |   id?: string, | 
					
						
							|  |  |  |  |   vslug?: string, | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   video?: number | VideoEntry, | 
					
						
							| 
									
										
										
										
											2021-01-04 16:31:36 +00:00
										 |  |  |  |   volume?: number, | 
					
						
							|  |  |  |  |   redirect?: boolean, | 
					
						
							|  |  |  |  |   title?: string, | 
					
						
							|  |  |  |  |   hlsServerURL?: string, | 
					
						
							|  |  |  |  |   dashServerURL?: string, | 
					
						
							|  |  |  |  |   basePath?: string, | 
					
						
							|  |  |  |  |   twitchPlayerParentKey?: string, | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | const getProps = withSession(async (req, _res, { id, vslug }: VideoPlayerPageParameters): Promise<GetServerSidePropsResult<VideoPlayerPageProps>> => { | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   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, | 
					
						
							| 
									
										
										
										
											2021-07-09 08:59:46 +00:00
										 |  |  |  |         vslug: video.slug, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |       }, | 
					
						
							|  |  |  |  |     }; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // Check if vslug is actually point to a file name
 | 
					
						
							| 
									
										
										
										
											2021-01-03 17:42:37 +00:00
										 |  |  |  |   const sanitizedFileName = `${sanitizeFileName(basename(vslug, '.mp4'))}.mp4`; | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   const realVIndex = videos.findIndex( | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     (video: VideoEntry) => video.downloadFileName === sanitizedFileName, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   ); | 
					
						
							|  |  |  |  |   if (realVIndex >= 0) { | 
					
						
							|  |  |  |  |     const video = videos[realVIndex]; | 
					
						
							|  |  |  |  |     return { | 
					
						
							|  |  |  |  |       props: { | 
					
						
							|  |  |  |  |         redirect: true, | 
					
						
							|  |  |  |  |         id, | 
					
						
							|  |  |  |  |         video: realVIndex, | 
					
						
							| 
									
										
										
										
											2021-07-09 08:59:46 +00:00
										 |  |  |  |         vslug: video.slug, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |       }, | 
					
						
							|  |  |  |  |     }; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // Check if we can find any video with matching vslug
 | 
					
						
							| 
									
										
										
										
											2021-07-09 08:59:46 +00:00
										 |  |  |  |   const video = videos.find(({ slug }: VideoEntry) => slug === vslug); | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   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())}`;
 | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   // TODO - detect HTTPS properly
 | 
					
						
							|  |  |  |  |   // NOTE - req.socket.encrypted fails in TypeScript due to typing
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   const basePath = `https://${req.headers.host}`; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   // 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; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |   if (!req.headers.host) { | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     throw new Error(JSON.stringify(req.headers)); | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   // Pass data to the page via props
 | 
					
						
							|  |  |  |  |   return { | 
					
						
							|  |  |  |  |     props: { | 
					
						
							|  |  |  |  |       id, | 
					
						
							|  |  |  |  |       vslug, | 
					
						
							|  |  |  |  |       video, | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |       volume, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |       title, | 
					
						
							|  |  |  |  |       hlsServerURL, | 
					
						
							|  |  |  |  |       dashServerURL, | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |       twitchPlayerParentKey: req.headers.host.split(':')[0], | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |       basePath, | 
					
						
							|  |  |  |  |     }, | 
					
						
							|  |  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  | }); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  | export const getServerSideProps: GetServerSideProps<VideoPlayerPageProps, VideoPlayerPageParameters> = async ({ | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   req, | 
					
						
							|  |  |  |  |   res, | 
					
						
							|  |  |  |  |   params, | 
					
						
							|  |  |  |  | }) => getProps(req, res, params); | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  | export default function VideoPlayerPage({ | 
					
						
							|  |  |  |  |   id, | 
					
						
							|  |  |  |  |   vslug, | 
					
						
							|  |  |  |  |   video, | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   volume, | 
					
						
							|  |  |  |  |   // extra,
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   redirect, | 
					
						
							|  |  |  |  |   title, | 
					
						
							|  |  |  |  |   hlsServerURL, | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   dashServerURL, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   basePath, | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |   twitchPlayerParentKey, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | }: InferGetServerSidePropsType<typeof getServerSideProps>) { | 
					
						
							|  |  |  |  |   if (redirect) { | 
					
						
							|  |  |  |  |     const router = useRouter(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     React.useEffect(() => { | 
					
						
							|  |  |  |  |       router.push(`/${id}/${vslug}`); | 
					
						
							|  |  |  |  |     }); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return ( | 
					
						
							|  |  |  |  |       <p> | 
					
						
							|  |  |  |  |         You will be redirected | 
					
						
							|  |  |  |  |         {' '} | 
					
						
							|  |  |  |  |         <Link href="/[id]/[vslug]" as={`/${id}/${vslug}`}> | 
					
						
							|  |  |  |  |           <span>here</span> | 
					
						
							|  |  |  |  |         </Link> | 
					
						
							|  |  |  |  |         … | 
					
						
							|  |  |  |  |       </p> | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   if (!video) { | 
					
						
							|  |  |  |  |     return notFound(); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const intl = useIntl(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   const playerRef = React.useRef<VideoPlayer>(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   if (typeof video === 'number') { | 
					
						
							|  |  |  |  |     throw new Error('Logic error - video should not be a number'); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   const { | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     downloadFileName, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |     title: videoTitle, | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     sourceVideoID, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |     sourceVideoURL, | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     sourceVideoStart: sourceVideoStartStr, | 
					
						
							|  |  |  |  |     run, | 
					
						
							|  |  |  |  |     youtubeVideoID, | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   } = video; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   // convert possibly-string sourceVideoStart field to actual number
 | 
					
						
							|  |  |  |  |   let sourceVideoStart: number; | 
					
						
							|  |  |  |  |   if (typeof sourceVideoStartStr === 'string') { | 
					
						
							|  |  |  |  |     sourceVideoStart = parseInt(sourceVideoStartStr, 10); | 
					
						
							|  |  |  |  |   } else { | 
					
						
							|  |  |  |  |     sourceVideoStart = sourceVideoStartStr; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-03 17:52:45 +00:00
										 |  |  |  |   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); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   const tabs: Array<React.ReactElement<typeof Tab>> = []; | 
					
						
							|  |  |  |  |   let defaultTab = 'mirror-player'; | 
					
						
							|  |  |  |  |   const downloadButtons: Array<React.ReactElement> = []; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // Mirror player tab
 | 
					
						
							|  |  |  |  |   if (typeof downloadFileName === 'string' && downloadFileName.length > 0) { | 
					
						
							|  |  |  |  |     tabs.push( | 
					
						
							|  |  |  |  |       <Tab | 
					
						
							|  |  |  |  |         eventKey="mirror-player" | 
					
						
							|  |  |  |  |         title={intl.formatMessage({ | 
					
						
							|  |  |  |  |           id: 'VideoPlayerPage.tab.mirrorPlayer', | 
					
						
							|  |  |  |  |           description: 'Title for mirror player tab', | 
					
						
							|  |  |  |  |           defaultMessage: 'Mirror player', | 
					
						
							|  |  |  |  |         })} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         <VideoPlayer | 
					
						
							|  |  |  |  |           ref={playerRef} | 
					
						
							|  |  |  |  |           autoplay | 
					
						
							|  |  |  |  |           controls | 
					
						
							|  |  |  |  |           defaultVolume={volume} | 
					
						
							|  |  |  |  |           language="en" | 
					
						
							|  |  |  |  |           playbackRates={[ | 
					
						
							|  |  |  |  |             0.25, | 
					
						
							|  |  |  |  |             0.5, | 
					
						
							|  |  |  |  |             0.75, | 
					
						
							|  |  |  |  |             1, | 
					
						
							|  |  |  |  |             1.25, | 
					
						
							|  |  |  |  |             1.5, | 
					
						
							|  |  |  |  |             1.75, | 
					
						
							|  |  |  |  |             2, | 
					
						
							|  |  |  |  |           ]} | 
					
						
							| 
									
										
										
										
											2023-01-09 03:02:37 +00:00
										 |  |  |  |           // eslint-disable-next-line react/jsx-no-bind
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |           onVolumeChange={onVolumeChange} | 
					
						
							|  |  |  |  |           sources={[ | 
					
						
							|  |  |  |  |             { src: getHLSMasterURL(hlsServerURL, id, downloadFileName), type: 'application/x-mpegURL' }, | 
					
						
							|  |  |  |  |             { src: getDASHManifestURL(dashServerURL, id, downloadFileName), type: 'application/dash+xml' }, | 
					
						
							|  |  |  |  |             { src: getDownloadURL(id, downloadFileName), type: 'video/mp4' }, | 
					
						
							|  |  |  |  |           ]} | 
					
						
							|  |  |  |  |           aspectRatio="16:9" | 
					
						
							|  |  |  |  |           fill | 
					
						
							|  |  |  |  |         /> | 
					
						
							|  |  |  |  |       </Tab>, | 
					
						
							|  |  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2023-01-09 03:02:46 +00:00
										 |  |  |  |     downloadButtons.push(<DownloadButton | 
					
						
							|  |  |  |  |       id={id} | 
					
						
							|  |  |  |  |       fileName={getDownloadURL(id, downloadFileName)} | 
					
						
							|  |  |  |  |     />); | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // Twitch tab
 | 
					
						
							|  |  |  |  |   if (sourceVideoID) { | 
					
						
							|  |  |  |  |     const twitchEmbedURL = new URL('https://player.twitch.tv'); | 
					
						
							|  |  |  |  |     twitchEmbedURL.searchParams.append('video', sourceVideoID); | 
					
						
							|  |  |  |  |     twitchEmbedURL.searchParams.append('parent', twitchPlayerParentKey); | 
					
						
							|  |  |  |  |     twitchEmbedURL.searchParams.append('t', `${Math.floor(sourceVideoStart / 60)}m${sourceVideoStart % 60}s`); | 
					
						
							|  |  |  |  |     tabs.unshift( | 
					
						
							|  |  |  |  |       <Tab | 
					
						
							|  |  |  |  |         key="twitch-player" | 
					
						
							|  |  |  |  |         eventKey="twitch-player" | 
					
						
							|  |  |  |  |         title={intl.formatMessage({ | 
					
						
							|  |  |  |  |           id: 'VideoPlayerPage.tab.twitchPlayer', | 
					
						
							|  |  |  |  |           description: 'Title for Twitch player tab', | 
					
						
							|  |  |  |  |           defaultMessage: 'Twitch player', | 
					
						
							|  |  |  |  |         })} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         <Ratio aspectRatio="16x9"> | 
					
						
							|  |  |  |  |           <iframe | 
					
						
							|  |  |  |  |             src={twitchEmbedURL.toString()} | 
					
						
							|  |  |  |  |             title="Twitch embed" | 
					
						
							|  |  |  |  |             frameBorder={0} | 
					
						
							|  |  |  |  |             allowFullScreen | 
					
						
							|  |  |  |  |           /> | 
					
						
							|  |  |  |  |         </Ratio> | 
					
						
							|  |  |  |  |       </Tab>, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |     defaultTab = 'twitch-player'; | 
					
						
							|  |  |  |  |     const twitchWatchURL = new URL(sourceVideoURL); | 
					
						
							|  |  |  |  |     twitchWatchURL.searchParams.append('t', `${Math.floor(sourceVideoStart / 60)}m${sourceVideoStart % 60}s`); | 
					
						
							|  |  |  |  |     downloadButtons.push( | 
					
						
							|  |  |  |  |       <Button | 
					
						
							|  |  |  |  |         href={twitchWatchURL.toString()} | 
					
						
							|  |  |  |  |         target="blank" | 
					
						
							|  |  |  |  |         variant="twitch" | 
					
						
							|  |  |  |  |         key="watchOnTwitch" | 
					
						
							|  |  |  |  |       > | 
					
						
							| 
									
										
										
										
											2023-01-09 03:02:23 +00:00
										 |  |  |  |         <FontAwesomeIcon icon={faTwitch} className="mr-2" /> | 
					
						
							|  |  |  |  |         {' '} | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |         <FormattedMessage | 
					
						
							|  |  |  |  |           id="VideoPlayerPage.watchOnTwitch" | 
					
						
							|  |  |  |  |           description="Button below video that links to the exact position in the archived stream to watch it there." | 
					
						
							|  |  |  |  |           defaultMessage="Watch on Twitch" | 
					
						
							|  |  |  |  |         /> | 
					
						
							|  |  |  |  |       </Button>, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // YouTube tab
 | 
					
						
							|  |  |  |  |   if (youtubeVideoID) { | 
					
						
							|  |  |  |  |     const youtubeURL = new URL(youtubeVideoID, 'https://youtu.be/'); | 
					
						
							|  |  |  |  |     tabs.unshift( | 
					
						
							|  |  |  |  |       <Tab | 
					
						
							|  |  |  |  |         key="youtube-player" | 
					
						
							|  |  |  |  |         eventKey="youtube-player" | 
					
						
							|  |  |  |  |         title={intl.formatMessage({ | 
					
						
							|  |  |  |  |           id: 'VideoPlayerPage.tab.youtubePlayer', | 
					
						
							|  |  |  |  |           description: 'Title for YouTube player tab', | 
					
						
							|  |  |  |  |           defaultMessage: 'YouTube player', | 
					
						
							|  |  |  |  |         })} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         <YouTubePlayer | 
					
						
							|  |  |  |  |           className={[ | 
					
						
							|  |  |  |  |             'ratio', | 
					
						
							|  |  |  |  |             'ratio-16by9', | 
					
						
							|  |  |  |  |             'ratio-16x9', | 
					
						
							|  |  |  |  |           ].join(' ')} | 
					
						
							|  |  |  |  |           videoId={youtubeVideoID} | 
					
						
							|  |  |  |  |           opts={{ | 
					
						
							|  |  |  |  |             playerVars: { | 
					
						
							|  |  |  |  |               // https://developers.google.com/youtube/player_parameters
 | 
					
						
							|  |  |  |  |               autoplay: 1, | 
					
						
							|  |  |  |  |               color: 'white', | 
					
						
							|  |  |  |  |               start: youtubeURL.searchParams.has('t') | 
					
						
							|  |  |  |  |                 ? parseInt(youtubeURL.searchParams.get('t').toString(), 10) | 
					
						
							|  |  |  |  |                 : 0, | 
					
						
							|  |  |  |  |             }, | 
					
						
							|  |  |  |  |           }} | 
					
						
							|  |  |  |  |         /> | 
					
						
							|  |  |  |  |       </Tab>, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |     defaultTab = 'youtube-player'; | 
					
						
							|  |  |  |  |     downloadButtons.push( | 
					
						
							|  |  |  |  |       <Button | 
					
						
							|  |  |  |  |         href={youtubeURL.toString()} | 
					
						
							|  |  |  |  |         target="blank" | 
					
						
							|  |  |  |  |         variant="youtube" | 
					
						
							|  |  |  |  |         key="watchOnYouTube" | 
					
						
							|  |  |  |  |       > | 
					
						
							| 
									
										
										
										
											2023-01-09 03:02:23 +00:00
										 |  |  |  |         <FontAwesomeIcon icon={faYoutube} className="mr-2" /> | 
					
						
							|  |  |  |  |         {' '} | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |         <FormattedMessage | 
					
						
							|  |  |  |  |           id="VideoPlayerPage.watchOnYouTube" | 
					
						
							|  |  |  |  |           description="Button below video that links user to the YouTube upload of the VOD." | 
					
						
							|  |  |  |  |           defaultMessage="Watch on YouTube" | 
					
						
							|  |  |  |  |         /> | 
					
						
							|  |  |  |  |       </Button>, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   return ( | 
					
						
							|  |  |  |  |     <div> | 
					
						
							|  |  |  |  |       <Head> | 
					
						
							|  |  |  |  |         <title> | 
					
						
							| 
									
										
										
										
											2023-01-09 01:35:10 +00:00
										 |  |  |  |           {`${videoTitle} – ${title} – ${intl.formatMessage({ | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |             id: 'App.title', | 
					
						
							|  |  |  |  |             description: 'The full title of the website', | 
					
						
							|  |  |  |  |             defaultMessage: 'Games Done Quick Instant Archive', | 
					
						
							| 
									
										
										
										
											2023-01-09 01:35:10 +00:00
										 |  |  |  |           })}`}
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |         </title> | 
					
						
							|  |  |  |  |       </Head> | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       <Breadcrumb> | 
					
						
							| 
									
										
										
										
											2023-01-09 01:35:54 +00:00
										 |  |  |  |         <Link legacyBehavior passHref href="/"> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |           <Breadcrumb.Item> | 
					
						
							|  |  |  |  |             <FormattedMessage | 
					
						
							|  |  |  |  |               id="Breadcrumb.homeTitle" | 
					
						
							|  |  |  |  |               defaultMessage="GDQ Instant Archive" | 
					
						
							|  |  |  |  |               description="Root node text in breadcrumb" | 
					
						
							|  |  |  |  |             /> | 
					
						
							|  |  |  |  |           </Breadcrumb.Item> | 
					
						
							|  |  |  |  |         </Link> | 
					
						
							| 
									
										
										
										
											2023-01-09 01:35:54 +00:00
										 |  |  |  |         <Link legacyBehavior passHref href="/[id]" as={`/${id}`}> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |           <Breadcrumb.Item>{title}</Breadcrumb.Item> | 
					
						
							|  |  |  |  |         </Link> | 
					
						
							| 
									
										
										
										
											2023-01-09 01:35:54 +00:00
										 |  |  |  |         <Link legacyBehavior passHref href="/[id]/[vslug]" as={`/${id}/${vslug}`}> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |           <Breadcrumb.Item active>{videoTitle}</Breadcrumb.Item> | 
					
						
							|  |  |  |  |         </Link> | 
					
						
							|  |  |  |  |       </Breadcrumb> | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |       <Tabs | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |         defaultActiveKey={defaultTab} | 
					
						
							|  |  |  |  |         unmountOnExit | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |       > | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |         {tabs} | 
					
						
							| 
									
										
										
										
											2021-01-03 19:23:58 +00:00
										 |  |  |  |       </Tabs> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |       <h1 className="mt-3"> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |         {title} | 
					
						
							|  |  |  |  |         : | 
					
						
							|  |  |  |  |         {' '} | 
					
						
							|  |  |  |  |         {videoTitle} | 
					
						
							|  |  |  |  |       </h1> | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |       <h3 className="mb-3"> | 
					
						
							|  |  |  |  |         {run?.category} | 
					
						
							|  |  |  |  |       </h3> | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |       <Row className="mb-3"> | 
					
						
							|  |  |  |  |         <Col sm={12} md={7}> | 
					
						
							|  |  |  |  |           <ButtonGroup> | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |             {downloadButtons} | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |           </ButtonGroup> | 
					
						
							|  |  |  |  |         </Col> | 
					
						
							|  |  |  |  |         <Col sm={12} md={5}> | 
					
						
							|  |  |  |  |           <CopyField icon="share"> | 
					
						
							|  |  |  |  |             {[basePath, id, vslug].join('/')} | 
					
						
							|  |  |  |  |           </CopyField> | 
					
						
							|  |  |  |  |         </Col> | 
					
						
							|  |  |  |  |       </Row> | 
					
						
							| 
									
										
										
										
											2023-01-08 10:07:44 +00:00
										 |  |  |  |     </div> | 
					
						
							| 
									
										
										
										
											2020-08-22 20:25:57 +00:00
										 |  |  |  |   ); | 
					
						
							|  |  |  |  | } |