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-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0", "short-unique-id": "^5.2.0",
"srt-webvtt": "^2.0.0",
"ts-key-enum": "^3.0.13", "ts-key-enum": "^3.0.13",
"video.js": "^8.23.3", "video.js": "^8.23.3",
"zustand": "^4.3.2" "zustand": "^4.3.2"
@ -3504,6 +3505,12 @@
"node": ">=0.10.0" "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": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "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-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0", "short-unique-id": "^5.2.0",
"srt-webvtt": "^2.0.0",
"ts-key-enum": "^3.0.13", "ts-key-enum": "^3.0.13",
"video.js": "^8.23.3", "video.js": "^8.23.3",
"zustand": "^4.3.2" "zustand": "^4.3.2"

View File

@ -42,7 +42,10 @@ export const LoadingVideo = ({
height: "100%", height: "100%",
}} }}
> >
<CircularProgress color="secondary" /> {status !== "NOT_PUBLISHED" && (
<CircularProgress color="secondary" />
)}
{status && ( {status && (
<Typography <Typography
variant="subtitle2" variant="subtitle2"
@ -53,10 +56,10 @@ export const LoadingVideo = ({
textAlign: "center", textAlign: "center",
}} }}
> >
{status === "NOT_PUBLISHED" && (
{status === "NOT_PUBLISHED" ? (
<>Video file was not published. Please inform the publisher!</> <>Video file was not published. Please inform the publisher!</>
)} ) : status === "REFETCHING" ? (
{status === "REFETCHING" ? (
<> <>
<> <>
{getDownloadProgress( {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 sliderRef = useRef(null);
const [hoverX, setHoverX] = useState<number | null>(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 [showDuration, setShowDuration] = useState(0)
const onProgressChange = (_: any, value: number | number[]) => { const onProgressChange = (_: any, value: number | number[]) => {
if (!playerRef.current) return; if (!playerRef.current) return;
playerRef.current.currentTime(value as number);
playerRef.current?.currentTime(value as number);
}; };
const THUMBNAIL_DEBOUNCE = 500; const THUMBNAIL_DEBOUNCE = 500;
@ -68,31 +69,7 @@ export const ProgressSlider = ({progress, duration, playerRef, extractFrames}:
const debounceTimeoutRef = useRef<any>(null); const debounceTimeoutRef = useRef<any>(null);
const previousBlobUrlRef = useRef<string | null>(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 handleMouseMove = (e: React.MouseEvent) => {
const slider = sliderRef.current; const slider = sliderRef.current;
@ -140,6 +117,8 @@ console.log('thumbnailUrl', thumbnailUrl, hoverX)
} }
console.log('duration', duration)
return ( return (
<Box position="relative" sx={{ <Box position="relative" sx={{
width: '100%', 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 ( return (
<CustomFontTooltip <CustomFontTooltip
@ -241,16 +221,14 @@ export const VideoTime = ({videoRef, progress, isScreenSmall}: any) => {
<Typography <Typography
sx={{ sx={{
fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall, fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall,
color: "white", color: 'white',
visibility: !videoRef.current?.duration ? "hidden" : "visible", visibility: typeof duration !== 'number' ? 'hidden' : 'visible',
whiteSpace: "nowrap", whiteSpace: 'nowrap',
}} }}
> >
{videoRef.current?.duration ? formatTime(progress) : ""} {typeof duration === 'number' ? formatTime(progress) : ''}
{" / "} {' / '}
{videoRef.current?.duration {typeof duration === 'number' ? formatTime(duration) : ''}
? formatTime(videoRef.current?.duration)
: ""}
</Typography> </Typography>
</CustomFontTooltip> </CustomFontTooltip>
); );

View File

@ -1,4 +1,4 @@
import { Box } from "@mui/material"; import { Box, IconButton } from "@mui/material";
import { ControlsContainer } from "./VideoPlayer-styles"; import { ControlsContainer } from "./VideoPlayer-styles";
// import { MobileControlsBar } from "./MobileControlsBar"; // import { MobileControlsBar } from "./MobileControlsBar";
import { import {
@ -18,7 +18,6 @@ interface VideoControlsBarProps {
canPlay: boolean canPlay: boolean
isScreenSmall: boolean isScreenSmall: boolean
controlsHeight?: string controlsHeight?: string
videoRef:Ref<HTMLVideoElement>;
progress: number; progress: number;
duration: number duration: number
isPlaying: boolean; isPlaying: boolean;
@ -32,9 +31,13 @@ interface VideoControlsBarProps {
showControlsFullScreen: boolean; showControlsFullScreen: boolean;
isFullScreen: boolean; isFullScreen: boolean;
playerRef: any 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; const showMobileControls = isScreenSmall && canPlay;
@ -75,7 +78,7 @@ export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullSc
width: '100%' width: '100%'
}}> }}>
<ProgressSlider extractFrames={extractFrames} playerRef={playerRef} progress={progress} duration={duration} /> <ProgressSlider playerRef={playerRef} progress={progress} duration={duration} />
<Box sx={{ <Box sx={{
width: '100%', width: '100%',
display: 'flex' display: 'flex'
@ -87,12 +90,15 @@ export const VideoControlsBar = ({showControls, isFullScreen, showControlsFullSc
<VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} /> <VolumeControl onVolumeChange={onVolumeChange} volume={volume} sliderWidth={"100px"} />
<VideoTime videoRef={videoRef} progress={progress}/> <VideoTime progress={progress} duration={duration}/>
</Box> </Box>
<Box sx={controlGroupSX}> <Box sx={controlGroupSX}>
<PlaybackRate /> <PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton /> <ObjectFitButton />
<IconButton onClick={openSubtitleManager}>
sub
</IconButton>
<PictureInPictureButton /> <PictureInPictureButton />
<FullscreenButton toggleFullscreen={toggleFullscreen} /> <FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box> </Box>

View File

@ -10,6 +10,7 @@ import videojs from 'video.js';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
import Player from "video.js/dist/types/player"; import Player from "video.js/dist/types/player";
import { SubtitleManager } from "./SubtitleManager";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
@ -25,8 +26,8 @@ type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
} }
const videoStyles = { const videoStyles = {
videoContainer: { aspectRatio: "16 / 9" }, videoContainer: { },
video: { aspectRatio: "16 / 9" }, video: { },
}; };
async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') { async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') {
@ -125,12 +126,14 @@ export const VideoPlayer = ({
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null); const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("contain"); const [videoObjectFit] = useState<StretchVideoType>("contain");
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const { volume, setVolume } = useVideoStore((state) => ({ const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore((state) => ({
volume: state.playbackSettings.volume, volume: state.playbackSettings.volume,
setVolume: state.setVolume, setVolume: state.setVolume,
setPlaybackRate: state.setPlaybackRate,
playbackRate: state.playbackSettings.playbackRate
})); }));
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false)
const [videoCodec, setVideoCodec] = useState<null | false | string>(null) const [videoCodec, setVideoCodec] = useState<null | false | string>(null)
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const { setProgress } = useProgressStore(); const { setProgress } = useProgressStore();
@ -138,6 +141,7 @@ export const VideoPlayer = ({
const [duration, setDuration] = useState(0) const [duration, setDuration] = useState(0)
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showControls, setShowControls] = useState(false) const [showControls, setShowControls] = useState(false)
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false)
const { const {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -158,12 +162,14 @@ export const VideoPlayer = ({
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls, setAlwaysShowControls,
status, percentLoaded, status, percentLoaded,
showControlsFullScreen showControlsFullScreen,
} = useVideoPlayerController({ } = useVideoPlayerController({
autoPlay, autoPlay,
videoRef, playerRef,
qortalVideoResource, qortalVideoResource,
retryAttempts retryAttempts,
isPlayerInitialized
}); });
const hotkeyHandlers = useMemo( const hotkeyHandlers = useMemo(
@ -199,8 +205,12 @@ export const VideoPlayer = ({
const closeSubtitleManager = useCallback(()=> {
setIsOpenSubtitleManage(false)
}, [])
const openSubtitleManager = useCallback(()=> {
setIsOpenSubtitleManage(true)
}, [])
const videoLocation = useMemo(() => { const videoLocation = useMemo(() => {
if (!qortalVideoResource) return null; if (!qortalVideoResource) return null;
@ -208,23 +218,27 @@ export const VideoPlayer = ({
}, [qortalVideoResource]); }, [qortalVideoResource]);
useVideoPlayerHotKeys(hotkeyHandlers); useVideoPlayerHotKeys(hotkeyHandlers);
const updateProgress = () => { const updateProgress = useCallback(() => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; console.log('currentTime2')
if (!ref.current || !videoLocation) return; const player = playerRef?.current;
if (typeof ref.current.currentTime === "number") { if (!player || typeof player?.currentTime !== 'function') return;
setProgress(videoLocation, ref.current.currentTime);
setLocalProgress(ref.current.currentTime) const currentTime = player.currentTime();
} console.log('currentTime3', currentTime)
}; if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
useEffect(() => { setProgress(videoLocation, currentTime);
const ref = videoRef as React.RefObject<HTMLVideoElement>; setLocalProgress(currentTime);
if (!ref.current) return; }
if (ref.current) { }, [videoLocation]);
ref.current.volume = volume; // useEffect(() => {
} // const ref = videoRef as React.RefObject<HTMLVideoElement>;
// Only run on mount // if (!ref.current) return;
// eslint-disable-next-line react-hooks/exhaustive-deps // if (ref.current) {
}, []); // ref.current.volume = volume;
// }
// // Only run on mount
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, []);
const onPlay = useCallback(() => { const onPlay = useCallback(() => {
setIsPlaying(true); setIsPlaying(true);
@ -235,9 +249,14 @@ export const VideoPlayer = ({
}, [setIsPlaying]); }, [setIsPlaying]);
const onVolumeChangeHandler = useCallback( const onVolumeChangeHandler = useCallback(
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => { (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
const video = e.currentTarget; try {
const video = e.currentTarget;
console.log('onVolumeChangeHandler')
setVolume(video.volume); setVolume(video.volume);
setIsMuted(video.muted); setIsMuted(video.muted);
} catch (error) {
console.error('onVolumeChangeHandler', onVolumeChangeHandler)
}
}, },
[setIsMuted, setVolume] [setIsMuted, setVolume]
); );
@ -246,10 +265,11 @@ export const VideoPlayer = ({
const videoStylesContainer = useMemo(() => { const videoStylesContainer = useMemo(() => {
return { return {
cursor: !showControls && isFullscreen ? "none" : "auto", cursor: showControls ? 'auto' : 'none',
aspectRatio: '16 / 9',
...videoStyles?.videoContainer, ...videoStyles?.videoContainer,
}; };
}, [showControls, isFullscreen]); }, [showControls]);
console.log('isFullscreen', isFullscreen, showControlsFullScreen) console.log('isFullscreen', isFullscreen, showControlsFullScreen)
@ -276,24 +296,25 @@ export const VideoPlayer = ({
setIsLoading(false); setIsLoading(false);
}, [setIsLoading]) }, [setIsLoading])
useEffect(() => { useEffect(() => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; if(!isPlayerInitialized) return
if (!ref.current) return; const player = playerRef.current;
const video = ref.current; if (!player || typeof player.on !== 'function') return;
if (!video) return;
const handleLoadedMetadata = () => { const handleLoadedMetadata = () => {
if(video?.duration){ const duration = player.duration?.();
setDuration(video.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 enterFullscreen = () => {
const ref = containerRef?.current as any; const ref = containerRef?.current as any;
@ -318,41 +339,41 @@ export const VideoPlayer = ({
const canvasRef = useRef(null) const canvasRef = useRef(null)
const videoRefForCanvas = useRef<any>(null) const videoRefForCanvas = useRef<any>(null)
const extractFrames = useCallback(async (time: number): Promise<string | null> => { const extractFrames = useCallback( (time: number): void => {
const video = videoRefForCanvas?.current; // const video = videoRefForCanvas?.current;
const canvas: any = canvasRef.current; // const canvas: any = canvasRef.current;
if (!video || !canvas) return null; // if (!video || !canvas) return null;
// Avoid unnecessary resize if already correct // // Avoid unnecessary resize if already correct
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) { // if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth; // canvas.width = video.videoWidth;
canvas.height = video.videoHeight; // canvas.height = video.videoHeight;
} // }
const context = canvas.getContext("2d"); // const context = canvas.getContext("2d");
if (!context) return null; // if (!context) return null;
// If video is already near the correct time, don't seek again // // If video is already near the correct time, don't seek again
const threshold = 0.01; // 10ms threshold // const threshold = 0.01; // 10ms threshold
if (Math.abs(video.currentTime - time) > threshold) { // if (Math.abs(video.currentTime - time) > threshold) {
await new Promise<void>((resolve) => { // await new Promise<void>((resolve) => {
const onSeeked = () => resolve(); // const onSeeked = () => resolve();
video.addEventListener("seeked", onSeeked, { once: true }); // video.addEventListener("seeked", onSeeked, { once: true });
video.currentTime = time; // 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) // // Use a faster method for image export (optional tradeoff)
const blob = await new Promise<Blob | null>((resolve) => { // const blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob((blob: any) => resolve(blob), "image/webp", 0.7); // 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); if (hideTimeout.current) clearTimeout(hideTimeout.current);
}, [setShowControls]); }, [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(()=> { const videoLocactionStringified = useMemo(()=> {
return JSON.stringify(qortalVideoResource) return JSON.stringify(qortalVideoResource)
}, [qortalVideoResource]) }, [qortalVideoResource])
useEffect(() => { 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) const resource = JSON.parse(videoLocactionStringified)
let canceled = false; let canceled = false;
const setupPlayer = async () => { try {
const setupPlayer = async () => {
const type = await getVideoMimeTypeFromUrl(resource); const type = await getVideoMimeTypeFromUrl(resource);
if (canceled) return; if (canceled) return;
@ -413,6 +429,7 @@ useEffect(() => {
responsive: true, responsive: true,
fluid: true, fluid: true,
poster: startPlay ? "" : poster, poster: startPlay ? "" : poster,
aspectRatio: '16:9' ,
sources: [ sources: [
{ {
src: resourceUrl, src: resourceUrl,
@ -426,25 +443,75 @@ useEffect(() => {
if (!playerRef.current && ref.current) { if (!playerRef.current && ref.current) {
playerRef.current = videojs(ref.current, options, () => { playerRef.current = videojs(ref.current, options, () => {
setIsPlayerInitialized(true)
playerRef.current?.poster(''); 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?.play();
}); });
playerRef.current?.on('error', () => {
const error = playerRef.current?.error();
console.error('Video.js playback error:', error);
// Optional: display user-friendly message
});
} }
}; };
setupPlayer(); setupPlayer();
return () => { } catch (error) {
canceled = true; console.error('useEffect start player', error)
if (playerRef.current) { }
playerRef.current.dispose(); return () => {
playerRef.current = null; 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]); }, [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 ( 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 <VideoContainer
tabIndex={0} tabIndex={0}
style={videoStylesContainer} style={videoStylesContainer}
@ -458,7 +525,7 @@ useEffect(() => {
tabIndex={0} tabIndex={0}
className="video-js" className="video-js"
// src={isReady && startPlay ? resourceUrl || undefined : undefined} src={isReady && startPlay ? resourceUrl || undefined : undefined}
poster={startPlay ? "" : poster} poster={startPlay ? "" : poster}
onTimeUpdate={updateProgress} onTimeUpdate={updateProgress}
autoPlay={autoPlay} autoPlay={autoPlay}
@ -471,17 +538,18 @@ useEffect(() => {
onPause={onPause} onPause={onPause}
onVolumeChange={onVolumeChangeHandler} onVolumeChange={onVolumeChangeHandler}
controls={false} controls={false}
onLoadedMetadata={onLoadedMetadata}
/> />
<canvas ref={canvasRef} style={{ display: "none" }}></canvas> {/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
<video src={isReady && startPlay ? resourceUrl || undefined : undefined} ref={videoRefForCanvas} style={{ display: "none" }}></video>
{isReady && ( {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> </VideoContainer>
</>
); );
}; };

View File

@ -19,14 +19,15 @@ const maxSpeed = 4.0;
const speedChange = 0.25; const speedChange = 0.25;
interface UseVideoControls { interface UseVideoControls {
videoRef: Ref<HTMLVideoElement>; playerRef: any;
autoPlay?: boolean; autoPlay?: boolean;
qortalVideoResource: QortalGetMetadata; qortalVideoResource: QortalGetMetadata;
retryAttempts?: number; retryAttempts?: number;
isPlayerInitialized: boolean
} }
export const useVideoPlayerController = (props: UseVideoControls) => { export const useVideoPlayerController = (props: UseVideoControls) => {
const { autoPlay, videoRef, qortalVideoResource, retryAttempts } = props; const { autoPlay, playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized } = props;
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false) const [showControlsFullScreen, setShowControlsFullScreen] = useState(false)
@ -60,48 +61,67 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
}, [qortalVideoResource]); }, [qortalVideoResource]);
useEffect(() => { useEffect(() => {
if (videoLocation) { if (videoLocation && isPlayerInitialized) {
const ref = videoRef as React.RefObject<HTMLVideoElement>; console.log('hellohhhh5')
try {
const ref = playerRef as any;
if (!ref.current) return; if (!ref.current) return;
const savedProgress = getProgress(videoLocation); const savedProgress = getProgress(videoLocation);
console.log('savedProgress', savedProgress)
if (typeof savedProgress === "number") { 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( const [playbackRate, _setLocalPlaybackRate] = useState(
playbackSettings.playbackRate playbackSettings.playbackRate
); );
const updatePlaybackRate = useCallback( const updatePlaybackRate = useCallback(
(newSpeed: number) => { (newSpeed: number) => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; try {
if (!ref.current) return; 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( const increaseSpeed = useCallback(
(wrapOverflow = true) => { (wrapOverflow = true) => {
const changedSpeed = playbackRate + speedChange; try {
const changedSpeed = playbackSettings.playbackRate + speedChange;
const newSpeed = wrapOverflow const newSpeed = wrapOverflow
? changedSpeed ? changedSpeed
: Math.min(changedSpeed, maxSpeed); : Math.min(changedSpeed, maxSpeed);
updatePlaybackRate(newSpeed); updatePlaybackRate(newSpeed);
} catch (error) {
console.error('increaseSpeed', increaseSpeed)
}
}, },
[updatePlaybackRate, playbackRate] [updatePlaybackRate, playbackSettings.playbackRate]
); );
const decreaseSpeed = useCallback(() => { const decreaseSpeed = useCallback(() => {
updatePlaybackRate(playbackRate - speedChange); updatePlaybackRate(playbackSettings.playbackRate - speedChange);
}, [updatePlaybackRate, playbackRate]); }, [updatePlaybackRate, playbackSettings.playbackRate]);
const toggleAlwaysShowControls = useCallback(() => { const toggleAlwaysShowControls = useCallback(() => {
setAlwaysShowControls((prev) => !prev); setAlwaysShowControls((prev) => !prev);
@ -118,88 +138,139 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
const onVolumeChange = useCallback( const onVolumeChange = useCallback(
(_: any, value: number | number[]) => { (_: any, value: number | number[]) => {
const newVolume = value as number; try {
const ref = videoRef as React.RefObject<HTMLVideoElement>; const newVolume = value as number;
const ref = playerRef as any;
if (!ref.current) return; 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 toggleMute = useCallback(() => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; try {
if (!ref.current) return; 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( const changeVolume = useCallback(
(delta: number) => { (delta: number) => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; try {
if (!ref.current) return; const player = playerRef.current;
if (!player || typeof player.volume !== 'function') return;
// Get current volume directly from video element const currentVolume = player.volume(); // Get current volume (01)
const currentVolume = ref.current.volume; let newVolume = Math.max(0, Math.min(currentVolume + delta, 1));
let newVolume = Math.max(0, Math.min(currentVolume + delta, 1)); newVolume = +newVolume.toFixed(2); // Round to 2 decimal places
newVolume = +newVolume.toFixed(2);
ref.current.volume = newVolume; player.volume(newVolume); // Set new volume
ref.current.muted = false; 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 setProgressAbsolute = useCallback((percent: number) => { const setProgressRelative = useCallback((seconds: number) => {
const ref = videoRef as React.RefObject<HTMLVideoElement>; try {
const player = playerRef.current;
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return;
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(() => { const toggleObjectFit = useCallback(() => {
setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain"); setVideoObjectFit(videoObjectFit === "contain" ? "fill" : "contain");
}, [setVideoObjectFit]); }, [setVideoObjectFit]);
const togglePlay = useCallback(async () => { const togglePlay = useCallback(async () => {
const ref = videoRef as React.RefObject<HTMLVideoElement>;
if (!ref.current) return;
try {
if (!startedFetchRef.current) { if (!startedFetchRef.current) {
setStartedFetch(true); setStartedFetch(true);
startedFetchRef.current = true; startedFetchRef.current = true;
setStartPlay(true); setStartPlay(true);
return; return;
} }
if (isReady && ref.current) { const player = playerRef.current;
if (ref.current.paused) { if (!player) return;
ref.current.play(); if (isReady) {
} else { if (player.paused()) {
ref.current.pause(); try {
await player.play();
} catch (err) {
console.warn('Play failed:', err);
} }
} else {
player.pause();
} }
}, [ setStartedFetch, isReady]); }
} 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(() => { useEffect(() => {
if (autoPlay) togglePlay(); if (autoPlay) togglePlay();