started on subtitles

This commit is contained in:
PhilReact 2025-06-14 03:46:05 +03:00
parent 4a12707f62
commit b48b47ae1c
8 changed files with 515 additions and 222 deletions

7
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -42,7 +42,10 @@ export const LoadingVideo = ({
height: "100%",
}}
>
<CircularProgress color="secondary" />
{status !== "NOT_PUBLISHED" && (
<CircularProgress color="secondary" />
)}
{status && (
<Typography
variant="subtitle2"
@ -53,10 +56,10 @@ export const LoadingVideo = ({
textAlign: "center",
}}
>
{status === "NOT_PUBLISHED" && (
{status === "NOT_PUBLISHED" ? (
<>Video file was not published. Please inform the publisher!</>
)}
{status === "REFETCHING" ? (
) : status === "REFETCHING" ? (
<>
<>
{getDownloadProgress(

View File

@ -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 (
<Dialog
open={!!open}
fullWidth={true}
maxWidth={"md"}
sx={{
zIndex: 999990,
}}
slotProps={{
paper: {
elevation: 0,
},
}}
>
<DialogTitle>Subtitles</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
})}
>
<CloseIcon />
</IconButton>
{mode === 1 && (
<PublisherSubtitles
subtitles={subtitles}
publisherName={qortalMetadata.name}
setMode={setMode}
onSelect={onSelect}
/>
)}
{/* {mode === 2 && (
<CommunitySubtitles
link={open?.link}
name={open?.name}
mode={mode}
setMode={setMode}
username={username}
category={open?.category}
rootName={open?.rootName}
/>
)}
{mode === 4 && (
<MySubtitles
link={open?.link}
name={open?.name}
mode={mode}
setMode={setMode}
username={username}
title={title}
description={description}
setDescription={setDescription}
setTitle={setTitle}
/>
)} */}
</Dialog>
)
}
interface PublisherSubtitlesProps {
publisherName: string
subtitles: any[]
setMode: (val: number)=> void
onSelect: (subtitle: any)=> void
}
const PublisherSubtitles = ({
publisherName,
subtitles,
setMode,
onSelect,
}: PublisherSubtitlesProps) => {
return (
<>
<DialogContent>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "100%",
alignItems: "flex-start",
}}
>
<ButtonBase
sx={{
width: "100%",
}}
onClick={() => setMode(2)}
>
<Box
sx={{
p: 2,
border: "2px solid",
borderRadius: 2,
width: "100%",
}}
>
<Typography>Create new index</Typography>
</Box>
</ButtonBase>
</Box>
</DialogContent>
</>
);
};

View File

@ -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<number | null>(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<any>(null);
const previousBlobUrlRef = useRef<string | null>(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 (
<Box position="relative" sx={{
width: '100%',
@ -230,7 +209,8 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX)
);
};
export const VideoTime = ({videoRef, progress, isScreenSmall}: any) => {
export const VideoTime = ({progress, isScreenSmall, duration}: any) => {
return (
<CustomFontTooltip
@ -241,16 +221,14 @@ export const VideoTime = ({videoRef, progress, isScreenSmall}: any) => {
<Typography
sx={{
fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall,
color: "white",
visibility: !videoRef.current?.duration ? "hidden" : "visible",
whiteSpace: "nowrap",
color: 'white',
visibility: typeof duration !== 'number' ? 'hidden' : 'visible',
whiteSpace: 'nowrap',
}}
>
{videoRef.current?.duration ? formatTime(progress) : ""}
{" / "}
{videoRef.current?.duration
? formatTime(videoRef.current?.duration)
: ""}
{typeof duration === 'number' ? formatTime(progress) : ''}
{' / '}
{typeof duration === 'number' ? formatTime(duration) : ''}
</Typography>
</CustomFontTooltip>
);

View File

@ -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<HTMLVideoElement>;
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%'
}}>
<ProgressSlider extractFrames={extractFrames} playerRef={playerRef} progress={progress} duration={duration} />
<ProgressSlider playerRef={playerRef} progress={progress} duration={duration} />
<Box sx={{
width: '100%',
display: 'flex'
@ -87,12 +90,15 @@ export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullSc
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} />
<VideoTime videoRef={videoRef} progress={progress}/>
<VideoTime progress={progress} duration={duration}/>
</Box>
<Box sx={controlGroupSX}>
<PlaybackRate />
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton />
<IconButton onClick={openSubtitleManager}>
sub
</IconButton>
<PictureInPictureButton />
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>

View File

@ -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<RefObject<HTMLDivElement> | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("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<Player | null>(null);
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false)
const [videoCodec, setVideoCodec] = useState<null | false | string>(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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
// 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<HTMLVideoElement, Event>) => {
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<HTMLVideoElement>;
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<any>(null)
const extractFrames = useCallback(async (time: number): Promise<string | null> => {
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<void>((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<void>((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<Blob | null>((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<Blob | null>((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 (
<>
{/* <video controls src={"http://127.0.0.1:22393/arbitrary/VIDEO/a-test/MYTEST2_like_MYTEST2_vid_test-parallel_cSYmIk"} ref={videoRefForCanvas} ></video> */}
<VideoContainer
tabIndex={0}
style={videoStylesContainer}
@ -458,7 +525,7 @@ useEffect(() => {
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}
/>
<canvas ref={canvasRef} style={{ display: "none" }}></canvas>
<video src={isReady && startPlay ? resourceUrl || undefined : undefined} ref={videoRefForCanvas} style={{ display: "none" }}></video>
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
{isReady && (
<VideoControlsBar playerRef={playerRef} isFullScreen={isFullscreen} showControlsFullScreen={showControlsFullScreen} showControls={showControls} extractFrames={extractFrames} toggleFullscreen={toggleFullscreen} onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} videoRef={videoRef} duration={duration} progress={localProgress} />
<VideoControlsBar playbackRate={playbackRate} increaseSpeed={hotkeyHandlers.increaseSpeed}
decreaseSpeed={hotkeyHandlers.decreaseSpeed} playerRef={playerRef} isFullScreen={isFullscreen} showControlsFullScreen={showControlsFullScreen} showControls={showControls} extractFrames={extractFrames} toggleFullscreen={toggleFullscreen} onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} duration={duration} progress={localProgress} openSubtitleManager={openSubtitleManager} />
)}
<SubtitleManager close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} />
</VideoContainer>
</>
);
};

View File

@ -19,14 +19,15 @@ const maxSpeed = 4.0;
const speedChange = 0.25;
interface UseVideoControls {
videoRef: Ref<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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 (01)
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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<HTMLVideoElement>;
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();