mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
started on subtitles
This commit is contained in:
parent
4a12707f62
commit
b48b47ae1c
7
package-lock.json
generated
7
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
159
src/components/VideoPlayer/SubtitleManager.tsx
Normal file
159
src/components/VideoPlayer/SubtitleManager.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 (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<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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user