import updateVideoProgressById from '../api/actions/updateVideoProgressById'; import { SponsorBlockSegmentType, SponsorBlockType } from '../pages/Video'; import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useEffect, useRef, useState, } from 'react'; import formatTime from '../functions/formatTime'; import { useSearchParams } from 'react-router-dom'; import getApiUrl from '../configuration/getApiUrl'; import { useKeyPress } from '../functions/useKeypressHook'; import { VideoResponseType } from '../api/loader/loadVideoById'; const VIDEO_PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3]; type VideoTag = SyntheticEvent; export type SkippedSegmentType = { from: number; to: number; }; export type SponsorSegmentsSkippedType = Record; type Subtitle = { name: string; source: string; lang: string; media_url: string; }; type SubtitlesProp = { subtitles: Subtitle[]; }; const Subtitles = ({ subtitles }: SubtitlesProp) => { return subtitles.map((subtitle: Subtitle) => { let label = subtitle.name; if (subtitle.source === 'auto') { label += ' - auto'; } return ( ); }); }; const handleTimeUpdate = ( youtubeId: string, watched: boolean, sponsorBlock?: SponsorBlockType, setSponsorSegmentSkipped?: Dispatch>, onWatchStateChanged?: (status: boolean) => void, ) => async (videoTag: VideoTag) => { const currentTime = Number(videoTag.currentTarget.currentTime); if (sponsorBlock && sponsorBlock.segments) { sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => { const [from, to] = segment.segment; if (currentTime >= from && currentTime <= from + 0.3) { videoTag.currentTarget.currentTime = to; setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => { return { ...segments, [segment.UUID]: { from, to } }; }); } if (currentTime > to + 10) { setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => { return { ...segments, [segment.UUID]: { from: 0, to: 0 } }; }); } }); } if (currentTime < 10 && currentTime === Number(videoTag.currentTarget.duration)) return; if (Number((currentTime % 10).toFixed(1)) <= 0.2) { // Check progress every 10 seconds or else progress is checked a few times a second const videoProgressResponse = await updateVideoProgressById({ youtubeId, currentProgress: currentTime, }); const { data: videoProgressResponseData } = videoProgressResponse ?? {}; if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) { onWatchStateChanged?.(true); } } }; type VideoPlayerProps = { video: VideoResponseType; sponsorBlock?: SponsorBlockType; embed?: boolean; autoplay?: boolean; onWatchStateChanged?: (status: boolean) => void; onVideoEnd?: () => void; }; const VideoPlayer = ({ video, sponsorBlock, embed, autoplay = false, onWatchStateChanged, onVideoEnd, }: VideoPlayerProps) => { const videoRef = useRef(null); const [searchParams] = useSearchParams(); const searchParamVideoProgress = searchParams.get('t'); const volumeFromStorage = Number(localStorage.getItem('playerVolume') ?? 1); const playBackSpeedFromStorage = Number(localStorage.getItem('playerSpeed') || 1); const playBackSpeedIndex = VIDEO_PLAYBACK_SPEEDS.indexOf(playBackSpeedFromStorage) !== -1 ? VIDEO_PLAYBACK_SPEEDS.indexOf(playBackSpeedFromStorage) : 3; const [skippedSegments, setSkippedSegments] = useState({}); const [isMuted, setIsMuted] = useState(false); const [playbackSpeedIndex, setPlaybackSpeedIndex] = useState(playBackSpeedIndex); const [lastSubtitleTack, setLastSubtitleTack] = useState(0); const [showHelpDialog, setShowHelpDialog] = useState(false); const [showInfoDialog, setShowInfoDialog] = useState(false); const [infoDialogContent, setInfoDialogContent] = useState(''); const questionmarkPressed = useKeyPress('?'); const mutePressed = useKeyPress('m'); const fullscreenPressed = useKeyPress('f'); const subtitlesPressed = useKeyPress('c'); const increasePlaybackSpeedPressed = useKeyPress('>'); const decreasePlaybackSpeedPressed = useKeyPress('<'); const resetPlaybackSpeedPressed = useKeyPress('='); const arrowRightPressed = useKeyPress('ArrowRight'); const arrowLeftPressed = useKeyPress('ArrowLeft'); const videoId = video.youtube_id; const videoUrl = video.media_url; const videoThumbUrl = video.vid_thumb_url; const watched = video.player.watched; const duration = video.player.duration; const videoSubtitles = video.subtitles; let videoSrcProgress = Number(video.player?.position) > 0 ? Number(video.player?.position) : ''; if (searchParamVideoProgress !== null) { videoSrcProgress = searchParamVideoProgress; } const infoDialog = (content: string) => { setInfoDialogContent(content); setShowInfoDialog(true); setTimeout(() => { setShowInfoDialog(false); setInfoDialogContent(''); }, 500); }; const handleVideoEnd = ( youtubeId: string, watched: boolean, setSponsorSegmentSkipped?: Dispatch>, ) => async (videoTag: VideoTag) => { const currentTime = Number(videoTag.currentTarget.currentTime); const videoProgressResponse = await updateVideoProgressById({ youtubeId, currentProgress: currentTime, }); const { data: videoProgressResponseData } = videoProgressResponse; if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) { onWatchStateChanged?.(true); } setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => { const keys = Object.keys(segments); keys.forEach(uuid => { segments[uuid] = { from: 0, to: 0 }; }); return segments; }); onVideoEnd?.(); }; useEffect(() => { if (mutePressed) { setIsMuted(!isMuted); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mutePressed]); useEffect(() => { if (increasePlaybackSpeedPressed) { const newSpeed = playbackSpeedIndex + 1; if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeed]) { const speed = VIDEO_PLAYBACK_SPEEDS[newSpeed]; videoRef.current.playbackRate = speed; setPlaybackSpeedIndex(newSpeed); infoDialog(`${speed}x`); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [increasePlaybackSpeedPressed]); useEffect(() => { if (decreasePlaybackSpeedPressed) { const newSpeedIndex = playbackSpeedIndex - 1; if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) { const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]; videoRef.current.playbackRate = speed; setPlaybackSpeedIndex(newSpeedIndex); infoDialog(`${speed}x`); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [decreasePlaybackSpeedPressed]); useEffect(() => { if (resetPlaybackSpeedPressed) { const newSpeedIndex = 3; if (videoRef.current && VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]) { const speed = VIDEO_PLAYBACK_SPEEDS[newSpeedIndex]; videoRef.current.playbackRate = speed; setPlaybackSpeedIndex(newSpeedIndex); infoDialog(`${speed}x`); } } }, [resetPlaybackSpeedPressed]); useEffect(() => { if (fullscreenPressed) { if (videoRef.current && videoRef.current.requestFullscreen && !document.fullscreenElement) { videoRef.current.requestFullscreen().catch(e => { console.error(e); infoDialog('Unable to enter fullscreen'); }); } else { document.exitFullscreen().catch(e => { console.error(e); infoDialog('Unable to exit fullscreen'); }); } } }, [fullscreenPressed]); useEffect(() => { if (subtitlesPressed) { if (videoRef.current) { const tracks = [...videoRef.current.textTracks]; if (tracks.length === 0) { return; } const lastIndex = tracks.findIndex(x => x.mode === 'showing'); const active = tracks[lastIndex]; if (!active && lastSubtitleTack !== 0) { tracks[lastSubtitleTack - 1].mode = 'showing'; } else { if (active) { active.mode = 'hidden'; setLastSubtitleTack(lastIndex + 1); } } } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [subtitlesPressed]); useEffect(() => { if (arrowLeftPressed) { infoDialog('- 5 seconds'); } }, [arrowLeftPressed]); useEffect(() => { if (arrowRightPressed) { infoDialog('+ 5 seconds'); } }, [arrowRightPressed]); useEffect(() => { if (questionmarkPressed) { if (!showHelpDialog) { setTimeout(() => { setShowHelpDialog(false); }, 3000); } setShowHelpDialog(!showHelpDialog); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [questionmarkPressed]); return ( <>
Show help ?
Toggle mute m
Toggle fullscreen f
Toggle subtitles (if available) c
Increase speed >
Decrease speed <
Reset speed =
Back 5 seconds
Forward 5 seconds
{infoDialogContent}
{sponsorBlock?.is_enabled && ( <> {sponsorBlock.segments.length == 0 && (

This video doesn't have any sponsor segments added. To add a segment go to{' '} this video on YouTube {' '} and add a segment using the{' '} SponsorBlock {' '} extension.

)} {sponsorBlock.has_unlocked && (

This video has unlocked sponsor segments. Go to{' '} this video on YouTube {' '} and vote on the segments using the{' '} SponsorBlock {' '} extension.

)} {Object.values(skippedSegments).map(({ from, to }, index) => { return ( {from !== 0 && to !== 0 && (

Skipped sponsor segment from {formatTime(from)} to {formatTime(to)}.

)}
); })} )}
); }; export default VideoPlayer;