import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' import ReactDOM from 'react-dom' import { Box, IconButton, Slider } from '@mui/material' import { CircularProgress, Typography } from '@mui/material' import { Key } from 'ts-key-enum' import { PlayArrow, Pause, VolumeUp, Fullscreen, PictureInPicture, VolumeOff, Calculate } from '@mui/icons-material' import { styled } from '@mui/system' import { Refresh } from '@mui/icons-material' import { Menu, MenuItem } from '@mui/material' import { MoreVert as MoreIcon } from '@mui/icons-material' import { GlobalContext, getBaseApiReact } from '../../App' import { resourceKeySelector } from '../../atoms/global' import { useRecoilValue } from 'recoil' const VideoContainer = styled(Box)` position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; margin: 0px; padding: 0px; ` const VideoElement = styled('video')` width: 100%; height: auto; max-height: calc(100vh - 150px); background: rgb(33, 33, 33); ` const ControlsContainer = styled(Box)` position: absolute; display: flex; align-items: center; justify-content: space-between; bottom: 0; left: 0; right: 0; padding: 8px; background-color: rgba(0, 0, 0, 0.6); ` interface VideoPlayerProps { src?: string poster?: string name?: string identifier?: string service?: string autoplay?: boolean from?: string | null customStyle?: any user?: string } export const VideoPlayer: React.FC = ({ poster, name, identifier, service, autoplay = true, from = null, customStyle = {}, node }) => { const keyIdentifier = useMemo(()=> { if(name && identifier && service){ return `${service}-${name}-${identifier}` } else { return undefined } }, [service, name, identifier]) const download = useRecoilValue(resourceKeySelector(keyIdentifier)); const { downloadResource } = useContext(GlobalContext); const videoRef = useRef(null) const [playing, setPlaying] = useState(false) const [volume, setVolume] = useState(1) const [mutedVolume, setMutedVolume] = useState(1) const [isMuted, setIsMuted] = useState(false) const [progress, setProgress] = useState(0) const [isLoading, setIsLoading] = useState(false) const [canPlay, setCanPlay] = useState(false) const [startPlay, setStartPlay] = useState(false) const [isMobileView, setIsMobileView] = useState(false) const [playbackRate, setPlaybackRate] = useState(1) const [anchorEl, setAnchorEl] = useState(null) const reDownload = useRef(false) const resetVideoState = () => { // Reset all states to their initial values setPlaying(false); setVolume(1); setMutedVolume(1); setIsMuted(false); setProgress(0); setIsLoading(false); setCanPlay(false); setStartPlay(false); setIsMobileView(false); setPlaybackRate(1); setAnchorEl(null); // Reset refs to their initial values if (videoRef.current) { videoRef.current.pause(); // Ensure the video is paused videoRef.current.currentTime = 0; // Reset video progress } reDownload.current = false; }; const src = useMemo(() => { if(name && identifier && service){ return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}` } return '' }, [service, name, identifier]) useEffect(()=> { resetVideoState() }, [keyIdentifier]) const resourceStatus = useMemo(() => { return download?.status || {} }, [download]) const minSpeed = 0.25; const maxSpeed = 4.0; const speedChange = 0.25; const updatePlaybackRate = (newSpeed: number) => { if (videoRef.current) { if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed videoRef.current.playbackRate = newSpeed setPlaybackRate(newSpeed) } } const increaseSpeed = (wrapOverflow = true) => { const changedSpeed = playbackRate + speedChange let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed) if (videoRef.current) { updatePlaybackRate(newSpeed); } } const decreaseSpeed = () => { if (videoRef.current) { updatePlaybackRate(playbackRate - speedChange); } } const togglePlay = async () => { if (!videoRef.current) return setStartPlay(true) if (!src || resourceStatus?.status !== 'READY') { ReactDOM.flushSync(() => { setIsLoading(true) }) getSrc() } if (playing) { videoRef.current.pause() } else { videoRef.current.play() } setPlaying(!playing) } const onVolumeChange = (_: any, value: number | number[]) => { if (!videoRef.current) return videoRef.current.volume = value as number setVolume(value as number) setIsMuted(false) } const onProgressChange = (_: any, value: number | number[]) => { if (!videoRef.current) return videoRef.current.currentTime = value as number setProgress(value as number) if (!playing) { videoRef.current.play() setPlaying(true) } } const handleEnded = () => { setPlaying(false) } const updateProgress = () => { if (!videoRef.current) return setProgress(videoRef.current.currentTime) } const [isFullscreen, setIsFullscreen] = useState(false) const enterFullscreen = () => { if (!videoRef.current) return if (videoRef.current.requestFullscreen) { videoRef.current.requestFullscreen() } } const exitFullscreen = () => { if (document.exitFullscreen) { document.exitFullscreen() } } const toggleFullscreen = () => { isFullscreen ? exitFullscreen() : enterFullscreen() } useEffect(() => { const handleFullscreenChange = () => { setIsFullscreen(!!document.fullscreenElement) } document.addEventListener('fullscreenchange', handleFullscreenChange) return () => { document.removeEventListener('fullscreenchange', handleFullscreenChange) } }, []) const handleCanPlay = () => { setIsLoading(false) setCanPlay(true) } const getSrc = React.useCallback(async () => { if (!name || !identifier || !service) return try { downloadResource({ name, service, identifier }) } catch (error) { console.error(error) } }, [identifier, name, service]) function formatTime(seconds: number): string { seconds = Math.floor(seconds) let minutes: number | string = Math.floor(seconds / 60) let hours: number | string = Math.floor(minutes / 60) let remainingSeconds: number | string = seconds % 60 let remainingMinutes: number | string = minutes % 60 if (remainingSeconds < 10) { remainingSeconds = '0' + remainingSeconds } if (remainingMinutes < 10) { remainingMinutes = '0' + remainingMinutes } if (hours === 0) { hours = '' } else { hours = hours + ':' } return hours + remainingMinutes + ':' + remainingSeconds } const reloadVideo = () => { if (!videoRef.current) return const currentTime = videoRef.current.currentTime videoRef.current.src = src videoRef.current.load() videoRef.current.currentTime = currentTime if (playing) { videoRef.current.play() } } useEffect(() => { if ( resourceStatus?.status === 'DOWNLOADED' && reDownload?.current === false ) { getSrc() reDownload.current = true } }, [getSrc, resourceStatus]) const handleMenuOpen = (event: any) => { setAnchorEl(event.currentTarget) } const handleMenuClose = () => { setAnchorEl(null) } useEffect(() => { const videoWidth = videoRef?.current?.offsetWidth if (videoWidth && videoWidth <= 600) { setIsMobileView(true) } }, [canPlay]) const getDownloadProgress = (current: number, total: number) => { const progress = current / total * 100; return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%' } const mute = () => { setIsMuted(true) setMutedVolume(volume) setVolume(0) if (videoRef.current) videoRef.current.volume = 0 } const unMute = () => { setIsMuted(false) setVolume(mutedVolume) if (videoRef.current) videoRef.current.volume = mutedVolume } const toggleMute = () => { isMuted ? unMute() : mute(); } const changeVolume = (volumeChange: number) => { if (videoRef.current) { const minVolume = 0; const maxVolume = 1; let newVolume = volumeChange + volume newVolume = Math.max(newVolume, minVolume) newVolume = Math.min(newVolume, maxVolume) setIsMuted(false) setMutedVolume(newVolume) videoRef.current.volume = newVolume setVolume(newVolume); } } const setProgressRelative = (secondsChange: number) => { if (videoRef.current) { const currentTime = videoRef.current?.currentTime const minTime = 0 const maxTime = videoRef.current?.duration || 100 let newTime = currentTime + secondsChange; newTime = Math.max(newTime, minTime) newTime = Math.min(newTime, maxTime) videoRef.current.currentTime = newTime; setProgress(newTime); } } const setProgressAbsolute = (videoPercent: number) => { if (videoRef.current) { videoPercent = Math.min(videoPercent, 100) videoPercent = Math.max(videoPercent, 0) const finalTime = videoRef.current?.duration * videoPercent / 100 videoRef.current.currentTime = finalTime setProgress(finalTime); } } const keyboardShortcutsDown = (e: React.KeyboardEvent) => { e.preventDefault() switch (e.key) { case Key.Add: increaseSpeed(false); break; case '+': increaseSpeed(false); break; case '>': increaseSpeed(false); break; case Key.Subtract: decreaseSpeed(); break; case '-': decreaseSpeed(); break; case '<': decreaseSpeed(); break; case Key.ArrowLeft: { if (e.shiftKey) setProgressRelative(-300); else if (e.ctrlKey) setProgressRelative(-60); else if (e.altKey) setProgressRelative(-10); else setProgressRelative(-5); } break; case Key.ArrowRight: { if (e.shiftKey) setProgressRelative(300); else if (e.ctrlKey) setProgressRelative(60); else if (e.altKey) setProgressRelative(10); else setProgressRelative(5); } break; case Key.ArrowDown: changeVolume(-0.05); break; case Key.ArrowUp: changeVolume(0.05); break; } } const keyboardShortcutsUp = (e: React.KeyboardEvent) => { e.preventDefault() switch (e.key) { case ' ': togglePlay(); break; case 'm': toggleMute(); break; case 'f': enterFullscreen(); break; case Key.Escape: exitFullscreen(); break; case '0': setProgressAbsolute(0); break; case '1': setProgressAbsolute(10); break; case '2': setProgressAbsolute(20); break; case '3': setProgressAbsolute(30); break; case '4': setProgressAbsolute(40); break; case '5': setProgressAbsolute(50); break; case '6': setProgressAbsolute(60); break; case '7': setProgressAbsolute(70); break; case '8': setProgressAbsolute(80); break; case '9': setProgressAbsolute(90); break; } } return ( {isLoading && ( {resourceStatus && ( {resourceStatus?.status === 'REFETCHING' ? ( <> <> {getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)} <> Refetching data in 25 seconds ) : resourceStatus?.status === 'DOWNLOADED' ? ( <>Download Completed: building tutorial video... ) : resourceStatus?.status !== 'READY' ? ( <> {getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)} ) : ( <>Fetching tutorial from the Qortal Network... )} )} )} {((!src && !isLoading) || !startPlay) && ( { togglePlay() }} sx={{ cursor: 'pointer' }} > )} {isMobileView && canPlay ? ( <> {playing ? : } increaseSpeed()}> Speed: {playbackRate}x ) : canPlay ? ( <> {playing ? : } {progress && videoRef.current?.duration && formatTime(progress)}/ {progress && videoRef.current?.duration && formatTime(videoRef.current?.duration)} {isMuted ? : } increaseSpeed()} > Speed: {playbackRate}x ) : null} ) }