diff --git a/package-lock.json b/package-lock.json index f6143ec..56f6ef5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "short-unique-id": "^5.2.0", + "srt-webvtt": "^2.0.0", "ts-key-enum": "^3.0.13", "video.js": "^8.23.3", "zustand": "^4.3.2" @@ -3504,6 +3505,12 @@ "node": ">=0.10.0" } }, + "node_modules/srt-webvtt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/srt-webvtt/-/srt-webvtt-2.0.0.tgz", + "integrity": "sha512-G2Z7/Jf2NRKrmLYNSIhSYZZYE6OFlKXFp9Au2/zJBKgrioUzmrAys1x7GT01dwl6d2sEnqr5uahEIOd0JW/Rbw==", + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index 7649c95..57dc322 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", "short-unique-id": "^5.2.0", + "srt-webvtt": "^2.0.0", "ts-key-enum": "^3.0.13", "video.js": "^8.23.3", "zustand": "^4.3.2" diff --git a/src/components/VideoPlayer/LoadingVideo.tsx b/src/components/VideoPlayer/LoadingVideo.tsx index e5eb5a4..4d83a5b 100644 --- a/src/components/VideoPlayer/LoadingVideo.tsx +++ b/src/components/VideoPlayer/LoadingVideo.tsx @@ -42,7 +42,10 @@ export const LoadingVideo = ({ height: "100%", }} > - + {status !== "NOT_PUBLISHED" && ( + + + )} {status && ( - {status === "NOT_PUBLISHED" && ( + + {status === "NOT_PUBLISHED" ? ( <>Video file was not published. Please inform the publisher! - )} - {status === "REFETCHING" ? ( + ) : status === "REFETCHING" ? ( <> <> {getDownloadProgress( diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx new file mode 100644 index 0000000..08efddc --- /dev/null +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -0,0 +1,159 @@ +import { useCallback, useEffect, useState } from "react" +import { QortalGetMetadata } from "../../types/interfaces/resources" +import { Box, ButtonBase, Dialog, DialogContent, DialogTitle, IconButton, Typography } from "@mui/material" +import CloseIcon from "@mui/icons-material/Close"; +import { useListStore } from "../../state/lists"; +import { useResources } from "../../hooks/useResources"; +import { useGlobal } from "../../context/GlobalProvider"; +interface SubtitleManagerProps { + qortalMetadata: QortalGetMetadata + close: ()=> void + open: boolean +} +export const SubtitleManager = ({qortalMetadata, open, close}: SubtitleManagerProps) => { + const [mode, setMode] = useState(1) + const {lists} = useGlobal() + const {fetchResources} = useResources() + // const [subtitles, setSubtitles] = useState([]) + const subtitles = useListStore( + (state) => state.lists[`${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`]?.items || [] +); + const getPublishedSubtitles = useCallback(async ()=> { + try { + await fetchResources(qortalMetadata, `${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`, "BASE64"); + + } catch (error) { + console.error(error) + } + }, []) + + useEffect(()=> { + if(!qortalMetadata?.identifier || !qortalMetadata?.name || !qortalMetadata?.service) return + + // getPublishedSubtitles() + }, [qortalMetadata?.identifier, qortalMetadata?.service, qortalMetadata?.name, getPublishedSubtitles]) + + const handleClose = () => { + close() + setMode(1); + // setTitle(""); + // setDescription(""); + // setHasMetadata(false); + }; + + const onSelect = ()=> { + + } + return ( + + Subtitles + ({ + position: "absolute", + right: 8, + top: 8, + })} + > + + + {mode === 1 && ( + + )} + {/* {mode === 2 && ( + + )} + + {mode === 4 && ( + + )} */} + + ) +} + +interface PublisherSubtitlesProps { + publisherName: string + subtitles: any[] + setMode: (val: number)=> void + onSelect: (subtitle: any)=> void +} + +const PublisherSubtitles = ({ + publisherName, + subtitles, + setMode, + onSelect, +}: PublisherSubtitlesProps) => { + + return ( + <> + + + setMode(2)} + > + + Create new index + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/VideoPlayer/VideoControls.tsx b/src/components/VideoPlayer/VideoControls.tsx index f3c00c8..a909e60 100644 --- a/src/components/VideoPlayer/VideoControls.tsx +++ b/src/components/VideoPlayer/VideoControls.tsx @@ -50,7 +50,7 @@ export const ReloadButton = ({reloadVideo, isScreenSmall}: any) => { ); }; -export const ProgressSlider = ({progress, duration, playerRef, extractFrames}: any) => { +export const ProgressSlider = ({progress, duration, playerRef}: any) => { const sliderRef = useRef(null); const [hoverX, setHoverX] = useState(null); @@ -58,7 +58,8 @@ export const ProgressSlider = ({progress, duration, playerRef, extractFrames}: const [showDuration, setShowDuration] = useState(0) const onProgressChange = (_: any, value: number | number[]) => { if (!playerRef.current) return; - playerRef.current.currentTime(value as number); + + playerRef.current?.currentTime(value as number); }; const THUMBNAIL_DEBOUNCE = 500; @@ -68,31 +69,7 @@ export const ProgressSlider = ({progress, duration, playerRef, extractFrames}: const debounceTimeoutRef = useRef(null); const previousBlobUrlRef = useRef(null); - const debouncedExtract = useCallback( - (time: number, clientX: number) => { - const last = lastRequestedTimeRef.current; - console.log('hello101') - console.log('last', last) - if (last !== null && Math.abs(time - last) < THUMBNAIL_MIN_DIFF) return; - lastRequestedTimeRef.current = time; - console.log('hello102') - - extractFrames(time).then((blobUrl: string | null) => { - console.log('blobUrl', blobUrl) - if (!blobUrl) return; - - // Clean up previous blob URL - if (previousBlobUrlRef.current) { - URL.revokeObjectURL(previousBlobUrlRef.current); - } - - previousBlobUrlRef.current = blobUrl; - setThumbnailUrl(blobUrl); - - }); - }, - [extractFrames] - ); + const handleMouseMove = (e: React.MouseEvent) => { const slider = sliderRef.current; @@ -140,6 +117,8 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX) } + console.log('duration', duration) + return ( { +export const VideoTime = ({progress, isScreenSmall, duration}: any) => { + return ( { - {videoRef.current?.duration ? formatTime(progress) : ""} - {" / "} - {videoRef.current?.duration - ? formatTime(videoRef.current?.duration) - : ""} + {typeof duration === 'number' ? formatTime(progress) : ''} + {' / '} + {typeof duration === 'number' ? formatTime(duration) : ''} ); diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 39a2598..ebe588e 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -1,4 +1,4 @@ -import { Box } from "@mui/material"; +import { Box, IconButton } from "@mui/material"; import { ControlsContainer } from "./VideoPlayer-styles"; // import { MobileControlsBar } from "./MobileControlsBar"; import { @@ -18,7 +18,6 @@ interface VideoControlsBarProps { canPlay: boolean isScreenSmall: boolean controlsHeight?: string - videoRef:Ref; progress: number; duration: number isPlaying: boolean; @@ -32,9 +31,13 @@ interface VideoControlsBarProps { showControlsFullScreen: boolean; isFullScreen: boolean; playerRef: any + increaseSpeed: ()=> void + decreaseSpeed: ()=> void + playbackRate: number + openSubtitleManager: ()=> void } -export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, videoRef, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames}: VideoControlsBarProps) => { +export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; @@ -75,7 +78,7 @@ export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullSc width: '100%' }}> - + - + - + + + sub + diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index f2d3612..e09192d 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -10,6 +10,7 @@ import videojs from 'video.js'; import 'video.js/dist/video-js.css'; import Player from "video.js/dist/types/player"; +import { SubtitleManager } from "./SubtitleManager"; type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; @@ -25,8 +26,8 @@ type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; } const videoStyles = { - videoContainer: { aspectRatio: "16 / 9" }, - video: { aspectRatio: "16 / 9" }, + videoContainer: { }, + video: { }, }; async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') { @@ -125,12 +126,14 @@ export const VideoPlayer = ({ const containerRef = useRef | null>(null); const [videoObjectFit] = useState("contain"); const [isPlaying, setIsPlaying] = useState(false); - const { volume, setVolume } = useVideoStore((state) => ({ + const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore((state) => ({ volume: state.playbackSettings.volume, setVolume: state.setVolume, + setPlaybackRate: state.setPlaybackRate, + playbackRate: state.playbackSettings.playbackRate })); const playerRef = useRef(null); - + const [isPlayerInitialized, setIsPlayerInitialized] = useState(false) const [videoCodec, setVideoCodec] = useState(null) const [isMuted, setIsMuted] = useState(false); const { setProgress } = useProgressStore(); @@ -138,6 +141,7 @@ export const VideoPlayer = ({ const [duration, setDuration] = useState(0) const [isLoading, setIsLoading] = useState(true); const [showControls, setShowControls] = useState(false) + const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false) const { reloadVideo, togglePlay, @@ -158,12 +162,14 @@ export const VideoPlayer = ({ setProgressAbsolute, setAlwaysShowControls, status, percentLoaded, - showControlsFullScreen + showControlsFullScreen, + } = useVideoPlayerController({ autoPlay, - videoRef, + playerRef, qortalVideoResource, - retryAttempts + retryAttempts, + isPlayerInitialized }); const hotkeyHandlers = useMemo( @@ -199,8 +205,12 @@ export const VideoPlayer = ({ - - +const closeSubtitleManager = useCallback(()=> { + setIsOpenSubtitleManage(false) +}, []) + const openSubtitleManager = useCallback(()=> { + setIsOpenSubtitleManage(true) +}, []) const videoLocation = useMemo(() => { if (!qortalVideoResource) return null; @@ -208,23 +218,27 @@ export const VideoPlayer = ({ }, [qortalVideoResource]); useVideoPlayerHotKeys(hotkeyHandlers); - const updateProgress = () => { - const ref = videoRef as React.RefObject; - if (!ref.current || !videoLocation) return; - if (typeof ref.current.currentTime === "number") { - setProgress(videoLocation, ref.current.currentTime); - setLocalProgress(ref.current.currentTime) - } - }; - useEffect(() => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; - if (ref.current) { - ref.current.volume = volume; - } - // Only run on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const updateProgress = useCallback(() => { + console.log('currentTime2') + const player = playerRef?.current; + if (!player || typeof player?.currentTime !== 'function') return; + + const currentTime = player.currentTime(); + console.log('currentTime3', currentTime) + if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) { + setProgress(videoLocation, currentTime); + setLocalProgress(currentTime); + } +}, [videoLocation]); + // useEffect(() => { + // const ref = videoRef as React.RefObject; + // if (!ref.current) return; + // if (ref.current) { + // ref.current.volume = volume; + // } + // // Only run on mount + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, []); const onPlay = useCallback(() => { setIsPlaying(true); @@ -235,9 +249,14 @@ export const VideoPlayer = ({ }, [setIsPlaying]); const onVolumeChangeHandler = useCallback( (e: React.SyntheticEvent) => { - const video = e.currentTarget; + try { + const video = e.currentTarget; + console.log('onVolumeChangeHandler') setVolume(video.volume); setIsMuted(video.muted); + } catch (error) { + console.error('onVolumeChangeHandler', onVolumeChangeHandler) + } }, [setIsMuted, setVolume] ); @@ -246,10 +265,11 @@ export const VideoPlayer = ({ const videoStylesContainer = useMemo(() => { return { - cursor: !showControls && isFullscreen ? "none" : "auto", + cursor: showControls ? 'auto' : 'none', + aspectRatio: '16 / 9', ...videoStyles?.videoContainer, }; - }, [showControls, isFullscreen]); + }, [showControls]); console.log('isFullscreen', isFullscreen, showControlsFullScreen) @@ -276,24 +296,25 @@ export const VideoPlayer = ({ setIsLoading(false); }, [setIsLoading]) - useEffect(() => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; - const video = ref.current; - if (!video) return; + useEffect(() => { + if(!isPlayerInitialized) return + const player = playerRef.current; + if (!player || typeof player.on !== 'function') return; - const handleLoadedMetadata = () => { - if(video?.duration){ - setDuration(video.duration) - } - }; + const handleLoadedMetadata = () => { + const duration = player.duration?.(); + if (typeof duration === 'number' && !isNaN(duration)) { + setDuration(duration); + } + }; - video.addEventListener('loadedmetadata', handleLoadedMetadata); + player.on('loadedmetadata', handleLoadedMetadata); + + return () => { + player.off('loadedmetadata', handleLoadedMetadata); + }; +}, [isPlayerInitialized]); - return () => { - video.removeEventListener('loadedmetadata', handleLoadedMetadata); - }; - }, []); const enterFullscreen = () => { const ref = containerRef?.current as any; @@ -318,41 +339,41 @@ export const VideoPlayer = ({ const canvasRef = useRef(null) const videoRefForCanvas = useRef(null) -const extractFrames = useCallback(async (time: number): Promise => { - const video = videoRefForCanvas?.current; - const canvas: any = canvasRef.current; +const extractFrames = useCallback( (time: number): void => { + // const video = videoRefForCanvas?.current; + // const canvas: any = canvasRef.current; - if (!video || !canvas) return null; + // if (!video || !canvas) return null; - // Avoid unnecessary resize if already correct - if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - } + // // Avoid unnecessary resize if already correct + // if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { + // canvas.width = video.videoWidth; + // canvas.height = video.videoHeight; + // } - const context = canvas.getContext("2d"); - if (!context) return null; + // const context = canvas.getContext("2d"); + // if (!context) return null; - // If video is already near the correct time, don't seek again - const threshold = 0.01; // 10ms threshold - if (Math.abs(video.currentTime - time) > threshold) { - await new Promise((resolve) => { - const onSeeked = () => resolve(); - video.addEventListener("seeked", onSeeked, { once: true }); - video.currentTime = time; - }); - } + // // If video is already near the correct time, don't seek again + // const threshold = 0.01; // 10ms threshold + // if (Math.abs(video.currentTime - time) > threshold) { + // await new Promise((resolve) => { + // const onSeeked = () => resolve(); + // video.addEventListener("seeked", onSeeked, { once: true }); + // video.currentTime = time; + // }); + // } - context.drawImage(video, 0, 0, canvas.width, canvas.height); + // context.drawImage(video, 0, 0, canvas.width, canvas.height); - // Use a faster method for image export (optional tradeoff) - const blob = await new Promise((resolve) => { - canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); - }); + // // Use a faster method for image export (optional tradeoff) + // const blob = await new Promise((resolve) => { + // canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); + // }); - if (!blob) return null; + // if (!blob) return null; - return URL.createObjectURL(blob); + // return URL.createObjectURL(blob); }, []); @@ -385,25 +406,20 @@ useEffect(() => { if (hideTimeout.current) clearTimeout(hideTimeout.current); }, [setShowControls]); - const onLoadedMetadata= (e: any)=> { - console.log('eeeeeeeeeee', e) - const ref = videoRef as any; - if (!ref.current) return; - console.log('datataa', ref.current.audioTracks , // List of available audio tracks -ref.current.textTracks , // Subtitles/closed captions -ref.current.videoTracks ) - } const videoLocactionStringified = useMemo(()=> { return JSON.stringify(qortalVideoResource) }, [qortalVideoResource]) useEffect(() => { - if (!resourceUrl || !isReady || !videoLocactionStringified) return; + if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return; + console.log("EFFECT TRIGGERED", { isReady, resourceUrl, startPlay, poster, videoLocactionStringified }); + const resource = JSON.parse(videoLocactionStringified) let canceled = false; - const setupPlayer = async () => { + try { + const setupPlayer = async () => { const type = await getVideoMimeTypeFromUrl(resource); if (canceled) return; @@ -413,6 +429,7 @@ useEffect(() => { responsive: true, fluid: true, poster: startPlay ? "" : poster, + aspectRatio: '16:9' , sources: [ { src: resourceUrl, @@ -426,25 +443,75 @@ useEffect(() => { if (!playerRef.current && ref.current) { playerRef.current = videojs(ref.current, options, () => { + setIsPlayerInitialized(true) playerRef.current?.poster(''); + playerRef.current?.playbackRate(playbackRate) + playerRef.current?.volume(volume); + + playerRef.current?.addRemoteTextTrack({ + kind: 'subtitles', + src: 'http://127.0.0.1:22393/arbitrary/DOCUMENT/a-test/test-identifier', + srclang: 'en', + label: 'English', + default: true + }, true); playerRef.current?.play(); + }); + playerRef.current?.on('error', () => { + const error = playerRef.current?.error(); + console.error('Video.js playback error:', error); + // Optional: display user-friendly message + }); } }; setupPlayer(); - return () => { - canceled = true; - if (playerRef.current) { - playerRef.current.dispose(); - playerRef.current = null; + } catch (error) { + console.error('useEffect start player', error) + } + return () => { + console.log('canceled') + canceled = true; + const player = playerRef.current; + + if (player && typeof player.dispose === 'function') { + try { + player.dispose(); + } catch (err) { + console.error('Error disposing Video.js player:', err); } - }; + playerRef.current = null; + } +}; }, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]); - + useEffect(() => { + if(!isPlayerInitialized) return + const player = playerRef?.current; + console.log('player rate', player) + if (!player) return; + + const handleRateChange = () => { + const newRate = player?.playbackRate(); + console.log('Playback rate changed:', newRate); + if(newRate){ + setPlaybackRate(newRate); // or any other state/action + } + }; + + player.on('ratechange', handleRateChange); + + return () => { + player.off('ratechange', handleRateChange); + }; +}, [isPlayerInitialized]); + return ( + <> + {/* */} + { tabIndex={0} className="video-js" - // src={isReady && startPlay ? resourceUrl || undefined : undefined} + src={isReady && startPlay ? resourceUrl || undefined : undefined} poster={startPlay ? "" : poster} onTimeUpdate={updateProgress} autoPlay={autoPlay} @@ -471,17 +538,18 @@ useEffect(() => { onPause={onPause} onVolumeChange={onVolumeChangeHandler} controls={false} - onLoadedMetadata={onLoadedMetadata} /> - - + {/* */} + {isReady && ( - + )} - + + ); }; diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index 19d6bc5..dc6d627 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -19,14 +19,15 @@ const maxSpeed = 4.0; const speedChange = 0.25; interface UseVideoControls { - videoRef: Ref; + playerRef: any; autoPlay?: boolean; qortalVideoResource: QortalGetMetadata; retryAttempts?: number; + isPlayerInitialized: boolean } export const useVideoPlayerController = (props: UseVideoControls) => { - const { autoPlay, videoRef, qortalVideoResource, retryAttempts } = props; + const { autoPlay, playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized } = props; const [isFullscreen, setIsFullscreen] = useState(false); const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) @@ -60,48 +61,67 @@ export const useVideoPlayerController = (props: UseVideoControls) => { }, [qortalVideoResource]); useEffect(() => { - if (videoLocation) { - const ref = videoRef as React.RefObject; + if (videoLocation && isPlayerInitialized) { + console.log('hellohhhh5') + try { + const ref = playerRef as any; if (!ref.current) return; const savedProgress = getProgress(videoLocation); + console.log('savedProgress', savedProgress) if (typeof savedProgress === "number") { - ref.current.currentTime = savedProgress; + playerRef.current?.currentTime(savedProgress); + + } + } catch (error) { + console.error('line 74', error) } } - }, [videoLocation, getProgress]); + }, [videoLocation, getProgress, isPlayerInitialized]); const [playbackRate, _setLocalPlaybackRate] = useState( playbackSettings.playbackRate ); const updatePlaybackRate = useCallback( - (newSpeed: number) => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; + (newSpeed: number) => { + try { + const player = playerRef.current; + if (!player) return; + + if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; + + const clampedSpeed = Math.min(Math.max(newSpeed, minSpeed), maxSpeed); + player.playbackRate(clampedSpeed); // ✅ Video.js API + + // _setLocalPlaybackRate(clampedSpeed); + // setPlaybackRate(clampedSpeed); + } catch (error) { + console.error('updatePlaybackRate', error) + } + }, + [setPlaybackRate, _setLocalPlaybackRate, minSpeed, maxSpeed] +); - if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed; - ref.current.playbackRate = newSpeed; - _setLocalPlaybackRate(newSpeed); - setPlaybackRate(newSpeed); - }, - [setPlaybackRate, _setLocalPlaybackRate] - ); const increaseSpeed = useCallback( (wrapOverflow = true) => { - const changedSpeed = playbackRate + speedChange; + try { + const changedSpeed = playbackSettings.playbackRate + speedChange; const newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed); updatePlaybackRate(newSpeed); + } catch (error) { + console.error('increaseSpeed', increaseSpeed) + } }, - [updatePlaybackRate, playbackRate] + [updatePlaybackRate, playbackSettings.playbackRate] ); const decreaseSpeed = useCallback(() => { - updatePlaybackRate(playbackRate - speedChange); - }, [updatePlaybackRate, playbackRate]); + updatePlaybackRate(playbackSettings.playbackRate - speedChange); + }, [updatePlaybackRate, playbackSettings.playbackRate]); const toggleAlwaysShowControls = useCallback(() => { setAlwaysShowControls((prev) => !prev); @@ -118,88 +138,139 @@ export const useVideoPlayerController = (props: UseVideoControls) => { const onVolumeChange = useCallback( (_: any, value: number | number[]) => { - const newVolume = value as number; - const ref = videoRef as React.RefObject; + try { + const newVolume = value as number; + const ref = playerRef as any; if (!ref.current) return; - if (ref.current) ref.current.volume = newVolume; + if (ref.current) { + playerRef.current?.volume(newVolume); + + } + } catch (error) { + console.error('onVolumeChange', error) + } }, [] ); const toggleMute = useCallback(() => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; + try { + const ref = playerRef as any; + if (!ref.current?.muted) return; - ref.current.muted = !ref.current.muted; + ref.current?.muted(!ref.current?.muted) + } catch (error) { + console.error('toggleMute', toggleMute) + } }, []); const changeVolume = useCallback( - (delta: number) => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; - - // Get current volume directly from video element - const currentVolume = ref.current.volume; - let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); - newVolume = +newVolume.toFixed(2); - - ref.current.volume = newVolume; - ref.current.muted = false; - - }, - [] - ); + (delta: number) => { + try { + const player = playerRef.current; + if (!player || typeof player.volume !== 'function') return; + + const currentVolume = player.volume(); // Get current volume (0–1) + let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); + newVolume = +newVolume.toFixed(2); // Round to 2 decimal places + + player.volume(newVolume); // Set new volume + player.muted(false); // Ensure it's unmuted + } catch (error) { + console.error('changeVolume', error) + } + }, + [] +); + - const setProgressRelative = useCallback((seconds: number) => { - const ref = videoRef as React.RefObject; - const current = ref.current.currentTime; - const duration = ref.current.duration || 100; - const newTime = Math.max(0, Math.min(current + seconds, duration)); - ref.current.currentTime = newTime; - }, []); +const setProgressRelative = useCallback((seconds: number) => { + try { + const player = playerRef.current; + if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return; - const setProgressAbsolute = useCallback((percent: number) => { - const ref = videoRef as React.RefObject; + const current = player.currentTime(); + const duration = player.duration() || 100; + const newTime = Math.max(0, Math.min(current + seconds, duration)); + + player.currentTime(newTime); + } catch (error) { + console.error('setProgressRelative', error) + } +}, []); + + + const setProgressAbsolute = useCallback((percent: number) => { +try { + const player = playerRef.current; + if (!player || typeof player.duration !== 'function' || typeof player.currentTime !== 'function') return; + + const duration = player.duration(); + const clampedPercent = Math.min(100, Math.max(0, percent)); + const finalTime = (duration * clampedPercent) / 100; + + player.currentTime(finalTime); +} catch (error) { + console.error('setProgressAbsolute', error) +} +}, []); - if (!ref.current) return; - const finalTime = - (ref.current.duration * Math.min(100, Math.max(0, percent))) / 100; - ref.current.currentTime = finalTime; - }, []); const toggleObjectFit = useCallback(() => { setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain"); }, [setVideoObjectFit]); - const togglePlay = useCallback(async () => { - const ref = videoRef as React.RefObject; - if (!ref.current) return; - if (!startedFetchRef.current) { - setStartedFetch(true); - startedFetchRef.current = true; - setStartPlay(true); - return; - } - if (isReady && ref.current) { - if (ref.current.paused) { - ref.current.play(); - } else { - ref.current.pause(); - } - - } - }, [ setStartedFetch, isReady]); +const togglePlay = useCallback(async () => { + + + try { + if (!startedFetchRef.current) { + setStartedFetch(true); + startedFetchRef.current = true; + setStartPlay(true); + return; + } + const player = playerRef.current; + if (!player) return; + if (isReady) { + if (player.paused()) { + try { + await player.play(); + } catch (err) { + console.warn('Play failed:', err); + } + } else { + player.pause(); + } + } + } catch (error) { + console.error('togglePlay', error) + } +}, [setStartedFetch, isReady]); + + + const reloadVideo = useCallback(async () => { + try { + const player = playerRef.current; + if (!player || !isReady || !resourceUrl) return; + + const currentTime = player.currentTime(); + + player.src({ src: resourceUrl, type: 'video/mp4' }); // Adjust type if needed + player.load(); + + player.ready(() => { + player.currentTime(currentTime); + player.play().catch((err: any) => { + console.warn('Playback failed after reload:', err); + }); + }); + } catch (error) { + console.error(error) + } +}, [isReady, resourceUrl]); - const reloadVideo = useCallback(async () => { - const ref = videoRef as React.RefObject; - if (!ref?.current || !isReady || !resourceUrl) return; - const currentTime = ref.current.currentTime; - ref.current.src = resourceUrl; - ref.current.load(); - ref.current.currentTime = currentTime; - ref.current.play(); - }, [isReady, resourceUrl]); useEffect(() => { if (autoPlay) togglePlay();