mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-16 18:21:21 +00:00
760 lines
22 KiB
TypeScript
760 lines
22 KiB
TypeScript
import {
|
|
ReactEventHandler,
|
|
Ref,
|
|
RefObject,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
|
import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
|
|
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
|
|
import { useProgressStore, useVideoStore } from "../../state/video";
|
|
import { useVideoPlayerController } from "./useVideoPlayerController";
|
|
import { LoadingVideo } from "./LoadingVideo";
|
|
import { VideoControlsBar } from "./VideoControlsBar";
|
|
import videojs from "video.js";
|
|
import "video.js/dist/video-js.css";
|
|
|
|
import Player from "video.js/dist/types/player";
|
|
import {
|
|
Subtitle,
|
|
SubtitleManager,
|
|
SubtitlePublishedData,
|
|
} from "./SubtitleManager";
|
|
import { base64ToBlobUrl } from "../../utils/base64";
|
|
import convert from "srt-webvtt";
|
|
|
|
export async function srtBase64ToVttBlobUrl(
|
|
base64Srt: string
|
|
): Promise<string | null> {
|
|
try {
|
|
// Step 1: Convert base64 string to a Uint8Array
|
|
const binary = atob(base64Srt);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
|
|
// Step 2: Create a Blob from the Uint8Array with correct MIME type
|
|
const srtBlob = new Blob([bytes], { type: "application/x-subrip" });
|
|
console.log("srtBlob", srtBlob);
|
|
// Step 3: Use convert() with the Blob
|
|
const vttBlobUrl: string = await convert(srtBlob);
|
|
return vttBlobUrl;
|
|
} catch (error) {
|
|
console.error("Failed to convert SRT to VTT:", error);
|
|
return null;
|
|
}
|
|
}
|
|
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
|
|
|
interface VideoPlayerProps {
|
|
qortalVideoResource: QortalGetMetadata;
|
|
videoRef: Ref<HTMLVideoElement>;
|
|
retryAttempts?: number;
|
|
poster?: string;
|
|
autoPlay?: boolean;
|
|
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
|
|
}
|
|
|
|
const videoStyles = {
|
|
videoContainer: {},
|
|
video: {},
|
|
};
|
|
|
|
async function loadMediaInfo(wasmPath = "/MediaInfoModule.wasm") {
|
|
const mediaInfoModule = await import("mediainfo.js");
|
|
return await mediaInfoModule.default({
|
|
format: "JSON",
|
|
full: true,
|
|
locateFile: () => wasmPath,
|
|
});
|
|
}
|
|
|
|
async function getVideoMimeTypeFromUrl(
|
|
qortalVideoResource: any
|
|
): Promise<string | null> {
|
|
try {
|
|
const metadataResponse = await fetch(
|
|
`/arbitrary/metadata/${qortalVideoResource.service}/${qortalVideoResource.name}/${qortalVideoResource.identifier}`
|
|
);
|
|
const metadataData = await metadataResponse.json();
|
|
return metadataData?.mimeType || null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
// const mediaInfo = await loadMediaInfo();
|
|
// const chunkCache = new Map<string, Uint8Array>();
|
|
|
|
// let fileSize = 0;
|
|
// try {
|
|
// const headResp = await fetch(videoUrl, { method: 'HEAD' });
|
|
// const lengthHeader = headResp.headers.get('Content-Length');
|
|
// if (!lengthHeader) throw new Error('Missing content length');
|
|
// fileSize = parseInt(lengthHeader, 10);
|
|
// } catch (err) {
|
|
// console.error('Error fetching content length:', err);
|
|
// return null;
|
|
// }
|
|
|
|
// try {
|
|
// const rawResult = await mediaInfo.analyzeData(
|
|
// () => fileSize,
|
|
// async (chunkSize: number, offset: number): Promise<Uint8Array> => {
|
|
// const key = `${offset}:${chunkSize}`;
|
|
// if (chunkCache.has(key)) return chunkCache.get(key)!;
|
|
|
|
// const end = Math.min(fileSize - 1, offset + chunkSize - 1);
|
|
// const resp = await fetch(videoUrl, {
|
|
// headers: { Range: `bytes=${offset}-${end}` },
|
|
// });
|
|
|
|
// if (!resp.ok || (resp.status !== 206 && fileSize > chunkSize)) {
|
|
// console.warn(`Range request failed: ${resp.status}`);
|
|
// return new Uint8Array();
|
|
// }
|
|
|
|
// const blob = await resp.blob();
|
|
// const buffer = new Uint8Array(await blob.arrayBuffer());
|
|
// chunkCache.set(key, buffer);
|
|
// return buffer;
|
|
// }
|
|
// );
|
|
|
|
// const result = JSON.parse(rawResult);
|
|
// const tracks = result?.media?.track;
|
|
|
|
// const videoTrack = tracks?.find((t: any) => t['@type'] === 'Video');
|
|
// const format = videoTrack?.Format?.toLowerCase();
|
|
|
|
// switch (format) {
|
|
// case 'avc':
|
|
// case 'h264':
|
|
// case 'mpeg-4':
|
|
// case 'mp4':
|
|
// return 'video/mp4';
|
|
// case 'vp8':
|
|
// case 'vp9':
|
|
// return 'video/webm';
|
|
// case 'hevc':
|
|
// case 'h265':
|
|
// return 'video/mp4'; // still usually wrapped in MP4
|
|
// case 'matroska':
|
|
// return 'video/webm';
|
|
// default:
|
|
// return 'video/mp4'; // fallback
|
|
// }
|
|
// } catch (err) {
|
|
// console.error('Error analyzing media info:', err);
|
|
// return null;
|
|
// }
|
|
}
|
|
|
|
export const VideoPlayer = ({
|
|
videoRef,
|
|
qortalVideoResource,
|
|
retryAttempts,
|
|
poster,
|
|
autoPlay,
|
|
onEnded,
|
|
}: VideoPlayerProps) => {
|
|
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
|
|
const [videoObjectFit] = useState<StretchVideoType>("contain");
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
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();
|
|
const [localProgress, setLocalProgress] = useState(0);
|
|
const [duration, setDuration] = useState(0);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [showControls, setShowControls] = useState(false);
|
|
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
|
|
const subtitleBtnRef = useRef(null);
|
|
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
|
|
const {
|
|
reloadVideo,
|
|
togglePlay,
|
|
onVolumeChange,
|
|
increaseSpeed,
|
|
decreaseSpeed,
|
|
toggleMute,
|
|
isFullscreen,
|
|
toggleObjectFit,
|
|
controlsHeight,
|
|
setProgressRelative,
|
|
toggleAlwaysShowControls,
|
|
changeVolume,
|
|
startedFetch,
|
|
isReady,
|
|
resourceUrl,
|
|
startPlay,
|
|
setProgressAbsolute,
|
|
setAlwaysShowControls,
|
|
status,
|
|
percentLoaded,
|
|
showControlsFullScreen,
|
|
} = useVideoPlayerController({
|
|
autoPlay,
|
|
playerRef,
|
|
qortalVideoResource,
|
|
retryAttempts,
|
|
isPlayerInitialized,
|
|
});
|
|
|
|
const hotkeyHandlers = useMemo(
|
|
() => ({
|
|
reloadVideo,
|
|
togglePlay,
|
|
setProgressRelative,
|
|
toggleObjectFit,
|
|
toggleAlwaysShowControls,
|
|
increaseSpeed,
|
|
decreaseSpeed,
|
|
changeVolume,
|
|
toggleMute,
|
|
setProgressAbsolute,
|
|
setAlwaysShowControls,
|
|
}),
|
|
[
|
|
reloadVideo,
|
|
togglePlay,
|
|
setProgressRelative,
|
|
toggleObjectFit,
|
|
toggleAlwaysShowControls,
|
|
increaseSpeed,
|
|
decreaseSpeed,
|
|
changeVolume,
|
|
toggleMute,
|
|
setProgressAbsolute,
|
|
setAlwaysShowControls,
|
|
]
|
|
);
|
|
|
|
const closeSubtitleManager = useCallback(() => {
|
|
setIsOpenSubtitleManage(false);
|
|
}, []);
|
|
const openSubtitleManager = useCallback(() => {
|
|
setIsOpenSubtitleManage(true);
|
|
}, []);
|
|
|
|
const videoLocation = useMemo(() => {
|
|
if (!qortalVideoResource) return null;
|
|
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
|
}, [qortalVideoResource]);
|
|
useVideoPlayerHotKeys(hotkeyHandlers);
|
|
|
|
const updateProgress = useCallback(() => {
|
|
const player = playerRef?.current;
|
|
if (!player || typeof player?.currentTime !== "function") return;
|
|
|
|
const currentTime = player.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);
|
|
}, [setIsPlaying]);
|
|
|
|
const onPause = useCallback(() => {
|
|
setIsPlaying(false);
|
|
}, [setIsPlaying]);
|
|
const onVolumeChangeHandler = useCallback(
|
|
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
|
try {
|
|
const video = e.currentTarget;
|
|
setVolume(video.volume);
|
|
setIsMuted(video.muted);
|
|
} catch (error) {
|
|
console.error("onVolumeChangeHandler", onVolumeChangeHandler);
|
|
}
|
|
},
|
|
[setIsMuted, setVolume]
|
|
);
|
|
|
|
const videoStylesContainer = useMemo(() => {
|
|
return {
|
|
cursor: showControls ? "auto" : "none",
|
|
aspectRatio: "16 / 9",
|
|
...videoStyles?.videoContainer,
|
|
};
|
|
}, [showControls]);
|
|
|
|
const videoStylesVideo = useMemo(() => {
|
|
return {
|
|
...videoStyles?.video,
|
|
objectFit: videoObjectFit,
|
|
backgroundColor: "#000000",
|
|
height: isFullscreen ? "calc(100vh - 40px)" : "100%",
|
|
width: "100%",
|
|
};
|
|
}, [videoObjectFit, isFullscreen]);
|
|
|
|
const handleEnded = useCallback(
|
|
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
|
if (onEnded) {
|
|
onEnded(e);
|
|
}
|
|
},
|
|
[onEnded]
|
|
);
|
|
|
|
const handleCanPlay = useCallback(() => {
|
|
setIsLoading(false);
|
|
}, [setIsLoading]);
|
|
|
|
useEffect(() => {
|
|
if (!isPlayerInitialized) return;
|
|
const player = playerRef.current;
|
|
if (!player || typeof player.on !== "function") return;
|
|
|
|
const handleLoadedMetadata = () => {
|
|
const duration = player.duration?.();
|
|
if (typeof duration === "number" && !isNaN(duration)) {
|
|
setDuration(duration);
|
|
}
|
|
};
|
|
|
|
player.on("loadedmetadata", handleLoadedMetadata);
|
|
|
|
return () => {
|
|
player.off("loadedmetadata", handleLoadedMetadata);
|
|
};
|
|
}, [isPlayerInitialized]);
|
|
|
|
const enterFullscreen = () => {
|
|
const ref = containerRef?.current as any;
|
|
if (!ref) return;
|
|
|
|
if (ref.requestFullscreen && !isFullscreen) {
|
|
ref.requestFullscreen();
|
|
}
|
|
};
|
|
|
|
const exitFullscreen = () => {
|
|
if (isFullscreen) document.exitFullscreen();
|
|
};
|
|
|
|
const toggleFullscreen = () => {
|
|
isFullscreen ? exitFullscreen() : enterFullscreen();
|
|
};
|
|
|
|
const canvasRef = useRef(null);
|
|
const videoRefForCanvas = useRef<any>(null);
|
|
const extractFrames = useCallback((time: number): void => {
|
|
// const video = videoRefForCanvas?.current;
|
|
// const canvas: any = canvasRef.current;
|
|
// 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;
|
|
// }
|
|
// 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;
|
|
// });
|
|
// }
|
|
// 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);
|
|
// });
|
|
// if (!blob) return null;
|
|
// return URL.createObjectURL(blob);
|
|
}, []);
|
|
|
|
const hideTimeout = useRef<any>(null);
|
|
|
|
const resetHideTimer = () => {
|
|
setShowControls(true);
|
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
|
hideTimeout.current = setTimeout(() => {
|
|
setShowControls(false);
|
|
}, 2500); // 3s of inactivity
|
|
};
|
|
|
|
const handleMouseMove = () => {
|
|
resetHideTimer();
|
|
};
|
|
|
|
useEffect(() => {
|
|
resetHideTimer(); // initial show
|
|
return () => {
|
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
|
};
|
|
}, []);
|
|
|
|
const previousSubtitleUrlRef = useRef<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
// Component unmount cleanup
|
|
if (previousSubtitleUrlRef.current) {
|
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
|
previousSubtitleUrlRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const onSelectSubtitle = useCallback(
|
|
async (subtitle: SubtitlePublishedData) => {
|
|
if(subtitle === null){
|
|
setCurrentSubTrack(null)
|
|
if (previousSubtitleUrlRef.current) {
|
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
|
previousSubtitleUrlRef.current = null;
|
|
}
|
|
const remoteTracksList = playerRef.current?.remoteTextTracks();
|
|
|
|
if (remoteTracksList) {
|
|
const toRemove: TextTrack[] = [];
|
|
|
|
// Bypass TS restrictions safely
|
|
const list = remoteTracksList as unknown as {
|
|
length: number;
|
|
[index: number]: TextTrack;
|
|
};
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
const track = list[i];
|
|
if (track) toRemove.push(track);
|
|
}
|
|
|
|
toRemove.forEach((track) => {
|
|
playerRef.current?.removeRemoteTextTrack(track);
|
|
});
|
|
}
|
|
|
|
return
|
|
}
|
|
console.log("onSelectSubtitle", subtitle);
|
|
const player = playerRef.current;
|
|
if (!player || !subtitle.subtitleData || !subtitle.type) return;
|
|
|
|
// Cleanup: revoke previous Blob URL
|
|
if (previousSubtitleUrlRef.current) {
|
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
|
previousSubtitleUrlRef.current = null;
|
|
}
|
|
let blobUrl;
|
|
if (subtitle?.type === "application/x-subrip") {
|
|
blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData);
|
|
} else {
|
|
blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type);
|
|
}
|
|
|
|
previousSubtitleUrlRef.current = blobUrl;
|
|
|
|
const remoteTracksList = playerRef.current?.remoteTextTracks();
|
|
|
|
if (remoteTracksList) {
|
|
const toRemove: TextTrack[] = [];
|
|
|
|
// Bypass TS restrictions safely
|
|
const list = remoteTracksList as unknown as {
|
|
length: number;
|
|
[index: number]: TextTrack;
|
|
};
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
const track = list[i];
|
|
if (track) toRemove.push(track);
|
|
}
|
|
|
|
toRemove.forEach((track) => {
|
|
playerRef.current?.removeRemoteTextTrack(track);
|
|
});
|
|
}
|
|
playerRef.current?.addRemoteTextTrack(
|
|
{
|
|
kind: "subtitles",
|
|
src: blobUrl,
|
|
srclang: subtitle.language,
|
|
label: subtitle.language,
|
|
default: true,
|
|
},
|
|
true
|
|
);
|
|
|
|
// Remove all existing remote text tracks
|
|
// try {
|
|
// const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_
|
|
// if (remoteTracks && remoteTracks?.length) {
|
|
// const toRemove: TextTrack[] = [];
|
|
// for (let i = 0; i < remoteTracks.length; i++) {
|
|
// const track = remoteTracks[i];
|
|
// toRemove.push(track);
|
|
// }
|
|
// toRemove.forEach((track) => {
|
|
// console.log('removing track')
|
|
// playerRef.current?.removeRemoteTextTrack(track);
|
|
// });
|
|
// }
|
|
// } catch (error) {
|
|
// console.log('error2', error)
|
|
// }
|
|
|
|
await new Promise((res) => {
|
|
setTimeout(() => {
|
|
res(null);
|
|
}, 1000);
|
|
});
|
|
const tracksInfo = playerRef.current?.textTracks();
|
|
console.log("tracksInfo", tracksInfo);
|
|
if (!tracksInfo) return;
|
|
|
|
const tracks = Array.from(
|
|
{ length: (tracksInfo as any).length },
|
|
(_, i) => (tracksInfo as any)[i]
|
|
);
|
|
console.log("tracks", tracks);
|
|
for (const track of tracks) {
|
|
console.log("track", track);
|
|
|
|
if (track.kind === "subtitles") {
|
|
track.mode = "showing"; // force display
|
|
}
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
setShowControls(false);
|
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
|
}, [setShowControls]);
|
|
|
|
const videoLocactionStringified = useMemo(() => {
|
|
return JSON.stringify(qortalVideoResource);
|
|
}, [qortalVideoResource]);
|
|
|
|
useEffect(() => {
|
|
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
|
|
return;
|
|
|
|
const resource = JSON.parse(videoLocactionStringified);
|
|
let canceled = false;
|
|
|
|
try {
|
|
const setupPlayer = async () => {
|
|
const type = await getVideoMimeTypeFromUrl(resource);
|
|
if (canceled) return;
|
|
|
|
const options = {
|
|
autoplay: true,
|
|
controls: false,
|
|
responsive: true,
|
|
fluid: true,
|
|
poster: startPlay ? "" : poster,
|
|
aspectRatio: "16:9",
|
|
sources: [
|
|
{
|
|
src: resourceUrl,
|
|
type: type || "video/mp4", // fallback
|
|
},
|
|
],
|
|
};
|
|
const ref = videoRef as any;
|
|
if (!ref.current) return;
|
|
|
|
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?.play();
|
|
|
|
const tracksInfo = playerRef.current?.textTracks();
|
|
|
|
const checkActiveSubtitle = () => {
|
|
let activeTrack = null;
|
|
|
|
const tracks = Array.from(
|
|
{ length: (tracksInfo as any).length },
|
|
(_, i) => (tracksInfo as any)[i]
|
|
);
|
|
console.log("tracks", tracks);
|
|
for (const track of tracks) {
|
|
|
|
if (track.kind === 'subtitles' || track.kind === 'captions') {
|
|
if (track.mode === 'showing') {
|
|
activeTrack = track;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeTrack) {
|
|
console.log("Subtitle active:", {
|
|
label: activeTrack.label,
|
|
srclang: activeTrack.language || activeTrack.srclang, // srclang for native, language for VTT
|
|
});
|
|
setCurrentSubTrack(activeTrack.language || activeTrack.srclang)
|
|
} else {
|
|
setCurrentSubTrack(null)
|
|
console.log("No subtitle is currently showing");
|
|
}
|
|
};
|
|
|
|
// Initial check in case one is auto-enabled
|
|
checkActiveSubtitle();
|
|
|
|
// Use Video.js event system
|
|
tracksInfo?.on("change", checkActiveSubtitle);
|
|
});
|
|
playerRef.current?.on("error", () => {
|
|
const error = playerRef.current?.error();
|
|
console.error("Video.js playback error:", error);
|
|
// Optional: display user-friendly message
|
|
});
|
|
}
|
|
};
|
|
|
|
setupPlayer();
|
|
} catch (error) {
|
|
console.error("useEffect start player", error);
|
|
}
|
|
return () => {
|
|
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;
|
|
if (!player) return;
|
|
|
|
const handleRateChange = () => {
|
|
const newRate = player?.playbackRate();
|
|
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}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
ref={containerRef}
|
|
>
|
|
<LoadingVideo
|
|
togglePlay={togglePlay}
|
|
isReady={isReady}
|
|
status={status}
|
|
percentLoaded={percentLoaded}
|
|
isLoading={isLoading}
|
|
/>
|
|
<VideoElement
|
|
ref={videoRef}
|
|
tabIndex={0}
|
|
className="video-js"
|
|
src={isReady && startPlay ? resourceUrl || undefined : undefined}
|
|
poster={startPlay ? "" : poster}
|
|
onTimeUpdate={updateProgress}
|
|
autoPlay={autoPlay}
|
|
onClick={togglePlay}
|
|
onEnded={handleEnded}
|
|
onCanPlay={handleCanPlay}
|
|
preload="metadata"
|
|
style={videoStylesVideo}
|
|
onPlay={onPlay}
|
|
onPause={onPause}
|
|
onVolumeChange={onVolumeChangeHandler}
|
|
controls={false}
|
|
/>
|
|
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
|
|
|
|
{isReady && (
|
|
<VideoControlsBar
|
|
subtitleBtnRef={subtitleBtnRef}
|
|
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
|
|
subtitleBtnRef={subtitleBtnRef}
|
|
close={closeSubtitleManager}
|
|
open={isOpenSubtitleManage}
|
|
qortalMetadata={qortalVideoResource}
|
|
onSelect={onSelectSubtitle}
|
|
currentSubTrack={currentSubTrack}
|
|
/>
|
|
</VideoContainer>
|
|
</>
|
|
);
|
|
};
|