switch sub

This commit is contained in:
PhilReact 2025-06-15 08:00:51 +03:00
parent d3c4a4713e
commit 864d87697c
3 changed files with 697 additions and 547 deletions

View File

@ -1,5 +1,9 @@
import React, { useCallback, useEffect, useState } from "react";
import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
QortalGetMetadata,
QortalMetadata,
Service,
} from "../../types/interfaces/resources";
import {
alpha,
Box,
@ -15,8 +19,10 @@ import {
Popover,
Typography,
} from "@mui/material";
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ModeEditIcon from '@mui/icons-material/ModeEdit';
import CheckIcon from '@mui/icons-material/Check';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import CloseIcon from "@mui/icons-material/Close";
import { useListStore } from "../../state/lists";
import { Resource, useResources } from "../../hooks/useResources";
@ -38,7 +44,8 @@ interface SubtitleManagerProps {
close: () => void;
open: boolean;
onSelect: (subtitle: SubtitlePublishedData) => void;
subtitleBtnRef: any
subtitleBtnRef: any;
currentSubTrack: null | string;
}
export interface Subtitle {
language: string | null;
@ -65,32 +72,38 @@ const SubtitleManagerComponent = ({
open,
close,
onSelect,
subtitleBtnRef
subtitleBtnRef,
currentSubTrack,
}: SubtitleManagerProps) => {
const [mode, setMode] = useState(1);
const { lists, identifierOperations, auth } = useGlobal();
const { fetchResources } = useResources();
// const [subtitles, setSubtitles] = useState([])
const subtitles = useListReturn(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`)
const subtitles = useListReturn(
`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`
);
console.log('subtitles222', subtitles)
console.log("subtitles222", subtitles);
const getPublishedSubtitles = useCallback(async () => {
try {
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
console.log('videoId', videoId)
console.log("videoId", videoId);
const postIdSearch = await identifierOperations.buildSearchPrefix(
ENTITY_SUBTITLE,
videoId,
videoId
);
const searchParams = {
service: SERVICE_SUBTITLE,
identifier: postIdSearch,
limit: 0
limit: 0,
};
const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64");
const res = await lists.fetchResources(
searchParams,
`subs-${videoId}`,
"BASE64"
);
lists.addList(`subs-${videoId}`, res || []);
console.log('resres2', res)
console.log("resres2", res);
} catch (error) {
console.error(error);
}
@ -104,7 +117,7 @@ const SubtitleManagerComponent = ({
)
return;
getPublishedSubtitles()
getPublishedSubtitles();
}, [
qortalMetadata?.identifier,
qortalMetadata?.service,
@ -120,67 +133,71 @@ const SubtitleManagerComponent = ({
// setHasMetadata(false);
};
const publishHandler = async (subtitles: Subtitle[]) => {
try {
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
const identifier = await identifierOperations.buildIdentifier(ENTITY_SUBTITLE, videoId);
const name = auth?.name
console.log('identifier2', identifier)
if(!name) return
const resources: ResourceToPublish[] = []
const tempResources: {qortalMetadata: QortalMetadata, data: any}[] = []
const identifier = await identifierOperations.buildIdentifier(
ENTITY_SUBTITLE,
videoId
);
const name = auth?.name;
console.log("identifier2", identifier);
if (!name) return;
const resources: ResourceToPublish[] = [];
const tempResources: { qortalMetadata: QortalMetadata; data: any }[] = [];
for (const sub of subtitles) {
const data = {
subtitleData: sub.base64,
language: sub.language,
filename: sub.filename,
type: sub.type
}
type: sub.type,
};
const base64Data = await objectToBase64(data)
const base64Data = await objectToBase64(data);
const resource = {
name,
identifier,
service: SERVICE_SUBTITLE,
base64: base64Data,
filename: sub.filename,
title: sub.language || undefined
}
resources.push(resource)
title: sub.language || undefined,
};
resources.push(resource);
tempResources.push({
qortalMetadata: {
identifier,
service: SERVICE_SUBTITLE,
name,
size: 100,
created: Date.now()
created: Date.now(),
},
data: data,
})
});
}
console.log('resources', resources)
console.log("resources", resources);
await qortalRequest({
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
resources
})
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources,
});
lists.addNewResources(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources)
} catch (error) {
}
lists.addNewResources(
`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`,
tempResources
);
} catch (error) {}
};
const onBack = () => {
if(mode === 1) close()
}
if (mode === 1) close();
};
const onSelectHandler = (sub: SubtitlePublishedData) => {
onSelect(sub)
close()
}
console.log('onSelectHandler')
onSelect(sub);
close();
};
return (
<Popover
open={!!open}
@ -195,8 +212,8 @@ console.log('identifier2', identifier)
},
paper: {
sx: {
bgcolor: alpha('#181818', 0.98),
color: 'white',
bgcolor: alpha("#181818", 0.98),
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: 5,
@ -206,38 +223,49 @@ console.log('identifier2', identifier)
},
}}
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
vertical: "top",
horizontal: "center",
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'center',
vertical: "bottom",
horizontal: "center",
}}
>
<Box
sx={{
padding: "5px 0px 10px 0px",
display: "flex",
gap: "10px",
width: "100%",
}}
>
<Box sx={{
padding: '5px 0px 10px 0px',
display: 'flex',
gap:'10px',
width: '100%'
}}>
<ButtonBase onClick={onBack}>
<ArrowBackIosIcon sx={{
fontSize: '1.15em'
}}/>
<ArrowBackIosIcon
sx={{
fontSize: "1.15em",
}}
/>
</ButtonBase>
<ButtonBase>
<Typography onClick={onBack} sx={{
fontSize: '0.85rem'
}}>Subtitles</Typography>
<Typography
onClick={onBack}
sx={{
fontSize: "0.85rem",
}}
>
Subtitles
</Typography>
</ButtonBase>
<ButtonBase sx={{
marginLeft: 'auto',
}}>
<ModeEditIcon sx={{
fontSize: '1.15rem'
}} />
<ButtonBase
sx={{
marginLeft: "auto",
}}
>
<ModeEditIcon
sx={{
fontSize: "1.15rem",
}}
/>
</ButtonBase>
</Box>
<Divider />
@ -248,6 +276,7 @@ console.log('identifier2', identifier)
setMode={setMode}
onSelect={onSelectHandler}
onBack={onBack}
currentSubTrack={currentSubTrack}
/>
)}
{/* <Box>
@ -345,6 +374,7 @@ interface PublisherSubtitlesProps {
setMode: (val: number) => void;
onSelect: (subtitle: any) => void;
onBack: () => void;
currentSubTrack: string | null
}
const PublisherSubtitles = ({
@ -352,28 +382,29 @@ const PublisherSubtitles = ({
subtitles,
setMode,
onSelect,
onBack
onBack,
currentSubTrack
}: PublisherSubtitlesProps) => {
return (
<>
{subtitles?.map((sub) => {
return <Subtitle onSelect={onSelect} sub={sub} key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}/>
return (
<Subtitle
currentSubtrack={currentSubTrack}
onSelect={onSelect}
sub={sub}
key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}
/>
);
})}
</>
);
};
interface PublishSubtitlesProps {
publishHandler: (subs: Subtitle[])=> void
publishHandler: (subs: Subtitle[]) => void;
}
const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
const [language, setLanguage] = useState<null | string>(null);
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
@ -388,7 +419,7 @@ const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
filename: file.name,
size: file.size,
};
newSubtitles.push(newSubtitle)
newSubtitles.push(newSubtitle);
} catch (error) {
console.error("Failed to parse audio file:", error);
}
@ -424,7 +455,7 @@ const onChangeValue = (field: string, data: any, index: number) => {
return copyPrev;
});
};
console.log('subtitles', subtitles)
console.log("subtitles", subtitles);
return (
<>
@ -441,8 +472,8 @@ console.log('subtitles', subtitles)
<Box {...getRootProps()}>
<Button
sx={{
display: 'flex',
gap: '10px',
display: "flex",
gap: "10px",
}}
variant="contained"
>
@ -455,7 +486,9 @@ console.log('subtitles', subtitles)
<>
<LanguageSelect
value={sub.language}
onChange={(val: string | null) => onChangeValue('language',val, i)}
onChange={(val: string | null) =>
onChangeValue("language", val, i)
}
/>
</>
);
@ -476,26 +509,38 @@ console.log('subtitles', subtitles)
};
interface SubProps {
sub: QortalGetMetadata
sub: QortalGetMetadata;
onSelect: (subtitle: Subtitle) => void;
currentSubtrack: null | string
}
const Subtitle = ({sub, onSelect}: SubProps)=> {
const {resource, isLoading } = usePublish(2, 'JSON', sub)
console.log('resource', resource)
return <Typography
onClick={()=> onSelect(resource?.data)}
sx={{
const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
const { resource, isLoading } = usePublish(2, "JSON", sub);
console.log("resource", resource);
const isSelected = currentSubtrack === resource?.data?.language
return (
<ButtonBase onClick={() => onSelect(isSelected ? null : resource?.data)} sx={{
px: 2,
py: 1,
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
}}
width: '100%',
justifyContent: 'space-between'
}}>
<Typography
>
{resource?.data?.language}
</Typography>
}
{isSelected ? (
<CheckIcon />
) : (
<ArrowForwardIosIcon />
)}
</ButtonBase>
);
};
export const SubtitleManager = React.memo(SubtitleManagerComponent);

View File

@ -65,6 +65,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
opacity: showControls ? 1 : 0,
pointerEvents: showControls ? 'auto' : 'none',
transition: 'opacity 0.4s ease-in-out',
width: '100%'
// ...additionalStyles
// height: controlsHeight,
}}
@ -94,7 +95,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
<VideoTime progress={progress} duration={duration}/>
</Box>
<Box sx={controlGroupSX}>
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton />
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>

View File

@ -1,4 +1,13 @@
import { ReactEventHandler, Ref, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
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";
@ -6,15 +15,21 @@ 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 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 {
Subtitle,
SubtitleManager,
SubtitlePublishedData,
} from "./SubtitleManager";
import { base64ToBlobUrl } from "../../utils/base64";
import convert from 'srt-webvtt';
import convert from "srt-webvtt";
export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise<string | null> {
export async function srtBase64ToVttBlobUrl(
base64Srt: string
): Promise<string | null> {
try {
// Step 1: Convert base64 string to a Uint8Array
const binary = atob(base64Srt);
@ -24,19 +39,18 @@ export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise<string |
}
// 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)
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
return vttBlobUrl;
} catch (error) {
console.error('Failed to convert SRT to VTT:', 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>;
@ -51,23 +65,26 @@ const videoStyles = {
video: {},
};
async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') {
const mediaInfoModule = await import('mediainfo.js');
async function loadMediaInfo(wasmPath = "/MediaInfoModule.wasm") {
const mediaInfoModule = await import("mediainfo.js");
return await mediaInfoModule.default({
format: 'JSON',
format: "JSON",
full: true,
locateFile: () => wasmPath,
});
}
async function getVideoMimeTypeFromUrl(qortalVideoResource: any): Promise<string | null> {
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
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
return null;
}
// const mediaInfo = await loadMediaInfo();
// const chunkCache = new Map<string, Uint8Array>();
@ -147,23 +164,26 @@ export const VideoPlayer = ({
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("contain");
const [isPlaying, setIsPlaying] = useState(false);
const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore((state) => ({
const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore(
(state) => ({
volume: state.playbackSettings.volume,
setVolume: state.setVolume,
setPlaybackRate: state.setPlaybackRate,
playbackRate: state.playbackSettings.playbackRate
}));
playbackRate: state.playbackSettings.playbackRate,
})
);
const playerRef = useRef<Player | null>(null);
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false)
const [videoCodec, setVideoCodec] = useState<null | false | string>(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 [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 [showControls, setShowControls] = useState(false);
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
const subtitleBtnRef = useRef(null);
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
const {
reloadVideo,
togglePlay,
@ -183,15 +203,15 @@ export const VideoPlayer = ({
startPlay,
setProgressAbsolute,
setAlwaysShowControls,
status, percentLoaded,
status,
percentLoaded,
showControlsFullScreen,
} = useVideoPlayerController({
autoPlay,
playerRef,
qortalVideoResource,
retryAttempts,
isPlayerInitialized
isPlayerInitialized,
});
const hotkeyHandlers = useMemo(
@ -223,16 +243,12 @@ export const VideoPlayer = ({
]
);
const closeSubtitleManager = useCallback(() => {
setIsOpenSubtitleManage(false)
}, [])
setIsOpenSubtitleManage(false);
}, []);
const openSubtitleManager = useCallback(() => {
setIsOpenSubtitleManage(true)
}, [])
setIsOpenSubtitleManage(true);
}, []);
const videoLocation = useMemo(() => {
if (!qortalVideoResource) return null;
@ -242,10 +258,10 @@ const closeSubtitleManager = useCallback(()=> {
const updateProgress = useCallback(() => {
const player = playerRef?.current;
if (!player || typeof player?.currentTime !== 'function') return;
if (!player || typeof player?.currentTime !== "function") return;
const currentTime = player.currentTime();
if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
if (typeof currentTime === "number" && videoLocation && currentTime > 0.1) {
setProgress(videoLocation, currentTime);
setLocalProgress(currentTime);
}
@ -274,30 +290,27 @@ const closeSubtitleManager = useCallback(()=> {
setVolume(video.volume);
setIsMuted(video.muted);
} catch (error) {
console.error('onVolumeChangeHandler', onVolumeChangeHandler)
console.error("onVolumeChangeHandler", onVolumeChangeHandler);
}
},
[setIsMuted, setVolume]
);
const videoStylesContainer = useMemo(() => {
return {
cursor: showControls ? 'auto' : 'none',
aspectRatio: '16 / 9',
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%'
width: "100%",
};
}, [videoObjectFit, isFullscreen]);
@ -312,28 +325,27 @@ const closeSubtitleManager = useCallback(()=> {
const handleCanPlay = useCallback(() => {
setIsLoading(false);
}, [setIsLoading])
}, [setIsLoading]);
useEffect(() => {
if(!isPlayerInitialized) return
if (!isPlayerInitialized) return;
const player = playerRef.current;
if (!player || typeof player.on !== 'function') return;
if (!player || typeof player.on !== "function") return;
const handleLoadedMetadata = () => {
const duration = player.duration?.();
if (typeof duration === 'number' && !isNaN(duration)) {
if (typeof duration === "number" && !isNaN(duration)) {
setDuration(duration);
}
};
player.on('loadedmetadata', handleLoadedMetadata);
player.on("loadedmetadata", handleLoadedMetadata);
return () => {
player.off('loadedmetadata', handleLoadedMetadata);
player.off("loadedmetadata", handleLoadedMetadata);
};
}, [isPlayerInitialized]);
const enterFullscreen = () => {
const ref = containerRef?.current as any;
if (!ref) return;
@ -341,8 +353,6 @@ const closeSubtitleManager = useCallback(()=> {
if (ref.requestFullscreen && !isFullscreen) {
ref.requestFullscreen();
}
};
const exitFullscreen = () => {
@ -353,23 +363,19 @@ const closeSubtitleManager = useCallback(()=> {
isFullscreen ? exitFullscreen() : enterFullscreen();
};
const canvasRef = useRef(null)
const videoRefForCanvas = useRef<any>(null)
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) {
@ -379,23 +385,17 @@ const extractFrames = useCallback( (time: number): void => {
// 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);
@ -427,33 +427,24 @@ useEffect(() => {
};
}, []);
const onSelectSubtitle = useCallback(async (subtitle: SubtitlePublishedData)=> {
console.log('onSelectSubtitle', subtitle)
const player = playerRef.current;
if (!player || !subtitle.subtitleData || !subtitle.type) return;
// Cleanup: revoke previous Blob URL
const onSelectSubtitle = useCallback(
async (subtitle: SubtitlePublishedData) => {
if(subtitle === null){
setCurrentSubTrack(null)
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 };
const list = remoteTracksList as unknown as {
length: number;
[index: number]: TextTrack;
};
for (let i = 0; i < list.length; i++) {
const track = list[i];
@ -464,13 +455,57 @@ if (remoteTracksList) {
playerRef.current?.removeRemoteTextTrack(track);
});
}
playerRef.current?.addRemoteTextTrack({
kind: 'subtitles',
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: 'en',
label: 'English',
default: true
}, true);
srclang: subtitle.language,
label: subtitle.language,
default: true,
},
true
);
// Remove all existing remote text tracks
// try {
@ -492,40 +527,43 @@ if (remoteTracksList) {
await new Promise((res) => {
setTimeout(() => {
res(null)
res(null);
}, 1000);
})
});
const tracksInfo = playerRef.current?.textTracks();
console.log('tracksInfo', tracksInfo)
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)
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)
console.log("track", track);
if (track.kind === 'subtitles') {
track.mode = 'showing'; // force display
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])
return JSON.stringify(qortalVideoResource);
}, [qortalVideoResource]);
useEffect(() => {
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return;
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
return;
const resource = JSON.parse(videoLocactionStringified)
const resource = JSON.parse(videoLocactionStringified);
let canceled = false;
try {
@ -539,11 +577,11 @@ useEffect(() => {
responsive: true,
fluid: true,
poster: startPlay ? "" : poster,
aspectRatio: '16:9' ,
aspectRatio: "16:9",
sources: [
{
src: resourceUrl,
type: type || 'video/mp4', // fallback
type: type || "video/mp4", // fallback
},
],
};
@ -552,37 +590,72 @@ useEffect(() => {
if (!playerRef.current && ref.current) {
playerRef.current = videojs(ref.current, options, () => {
setIsPlayerInitialized(true)
playerRef.current?.poster('');
playerRef.current?.playbackRate(playbackRate)
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
});
playerRef.current?.on('error', () => {
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);
console.error("Video.js playback error:", error);
// Optional: display user-friendly message
});
}
};
setupPlayer();
} catch (error) {
console.error('useEffect start player', error)
console.error("useEffect start player", error);
}
return () => {
canceled = true;
const player = playerRef.current;
if (player && typeof player.dispose === 'function') {
if (player && typeof player.dispose === "function") {
try {
player.dispose();
} catch (err) {
console.error('Error disposing Video.js player:', err);
console.error("Error disposing Video.js player:", err);
}
playerRef.current = null;
}
@ -590,7 +663,7 @@ useEffect(() => {
}, [isReady, resourceUrl, startPlay, poster, videoLocactionStringified]);
useEffect(() => {
if(!isPlayerInitialized) return
if (!isPlayerInitialized) return;
const player = playerRef?.current;
if (!player) return;
@ -601,10 +674,10 @@ useEffect(() => {
}
};
player.on('ratechange', handleRateChange);
player.on("ratechange", handleRateChange);
return () => {
player.off('ratechange', handleRateChange);
player.off("ratechange", handleRateChange);
};
}, [isPlayerInitialized]);
@ -619,12 +692,17 @@ useEffect(() => {
onMouseLeave={handleMouseLeave}
ref={containerRef}
>
<LoadingVideo togglePlay={togglePlay} isReady={isReady} status={status} percentLoaded={percentLoaded} isLoading={isLoading} />
<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}
@ -638,17 +716,43 @@ useEffect(() => {
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} />
<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} />
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={isOpenSubtitleManage}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
/>
</VideoContainer>
</>
);