mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-16 18:21:21 +00:00
switch sub
This commit is contained in:
parent
d3c4a4713e
commit
864d87697c
@ -1,7 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources";
|
|
||||||
import {
|
import {
|
||||||
alpha,
|
QortalGetMetadata,
|
||||||
|
QortalMetadata,
|
||||||
|
Service,
|
||||||
|
} from "../../types/interfaces/resources";
|
||||||
|
import {
|
||||||
|
alpha,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
@ -15,8 +19,10 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import ModeEditIcon from '@mui/icons-material/ModeEdit';
|
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 CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useListStore } from "../../state/lists";
|
import { useListStore } from "../../state/lists";
|
||||||
import { Resource, useResources } from "../../hooks/useResources";
|
import { Resource, useResources } from "../../hooks/useResources";
|
||||||
@ -31,14 +37,15 @@ import {
|
|||||||
} from "react-dropzone";
|
} from "react-dropzone";
|
||||||
import { fileToBase64, objectToBase64 } from "../../utils/base64";
|
import { fileToBase64, objectToBase64 } from "../../utils/base64";
|
||||||
import { ResourceToPublish } from "../../types/qortalRequests/types";
|
import { ResourceToPublish } from "../../types/qortalRequests/types";
|
||||||
import { useListReturn } from "../../hooks/useListData";
|
import { useListReturn } from "../../hooks/useListData";
|
||||||
import { usePublish } from "../../hooks/usePublish";
|
import { usePublish } from "../../hooks/usePublish";
|
||||||
interface SubtitleManagerProps {
|
interface SubtitleManagerProps {
|
||||||
qortalMetadata: QortalGetMetadata;
|
qortalMetadata: QortalGetMetadata;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onSelect: (subtitle: SubtitlePublishedData)=> void;
|
onSelect: (subtitle: SubtitlePublishedData) => void;
|
||||||
subtitleBtnRef: any
|
subtitleBtnRef: any;
|
||||||
|
currentSubTrack: null | string;
|
||||||
}
|
}
|
||||||
export interface Subtitle {
|
export interface Subtitle {
|
||||||
language: string | null;
|
language: string | null;
|
||||||
@ -65,32 +72,38 @@ const SubtitleManagerComponent = ({
|
|||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
onSelect,
|
onSelect,
|
||||||
subtitleBtnRef
|
subtitleBtnRef,
|
||||||
|
currentSubTrack,
|
||||||
}: SubtitleManagerProps) => {
|
}: SubtitleManagerProps) => {
|
||||||
const [mode, setMode] = useState(1);
|
const [mode, setMode] = useState(1);
|
||||||
const { lists, identifierOperations, auth } = useGlobal();
|
const { lists, identifierOperations, auth } = useGlobal();
|
||||||
const { fetchResources } = useResources();
|
const { fetchResources } = useResources();
|
||||||
// const [subtitles, setSubtitles] = useState([])
|
// 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 () => {
|
const getPublishedSubtitles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
||||||
console.log('videoId', videoId)
|
console.log("videoId", videoId);
|
||||||
const postIdSearch = await identifierOperations.buildSearchPrefix(
|
const postIdSearch = await identifierOperations.buildSearchPrefix(
|
||||||
ENTITY_SUBTITLE,
|
ENTITY_SUBTITLE,
|
||||||
videoId,
|
videoId
|
||||||
);
|
);
|
||||||
const searchParams = {
|
const searchParams = {
|
||||||
service: SERVICE_SUBTITLE,
|
service: SERVICE_SUBTITLE,
|
||||||
identifier: postIdSearch,
|
identifier: postIdSearch,
|
||||||
limit: 0
|
limit: 0,
|
||||||
};
|
};
|
||||||
const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64");
|
const res = await lists.fetchResources(
|
||||||
lists.addList(`subs-${videoId}`, res || []);
|
searchParams,
|
||||||
console.log('resres2', res)
|
`subs-${videoId}`,
|
||||||
|
"BASE64"
|
||||||
|
);
|
||||||
|
lists.addList(`subs-${videoId}`, res || []);
|
||||||
|
console.log("resres2", res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -104,7 +117,7 @@ const SubtitleManagerComponent = ({
|
|||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
getPublishedSubtitles()
|
getPublishedSubtitles();
|
||||||
}, [
|
}, [
|
||||||
qortalMetadata?.identifier,
|
qortalMetadata?.identifier,
|
||||||
qortalMetadata?.service,
|
qortalMetadata?.service,
|
||||||
@ -120,137 +133,153 @@ const SubtitleManagerComponent = ({
|
|||||||
// setHasMetadata(false);
|
// setHasMetadata(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const publishHandler = async (subtitles: Subtitle[]) => {
|
const publishHandler = async (subtitles: Subtitle[]) => {
|
||||||
try {
|
try {
|
||||||
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
||||||
|
|
||||||
const identifier = await identifierOperations.buildIdentifier(ENTITY_SUBTITLE, videoId);
|
const identifier = await identifierOperations.buildIdentifier(
|
||||||
const name = auth?.name
|
ENTITY_SUBTITLE,
|
||||||
console.log('identifier2', identifier)
|
videoId
|
||||||
if(!name) return
|
);
|
||||||
const resources: ResourceToPublish[] = []
|
const name = auth?.name;
|
||||||
const tempResources: {qortalMetadata: QortalMetadata, data: any}[] = []
|
console.log("identifier2", identifier);
|
||||||
for(const sub of subtitles ){
|
if (!name) return;
|
||||||
const data = {
|
const resources: ResourceToPublish[] = [];
|
||||||
subtitleData: sub.base64,
|
const tempResources: { qortalMetadata: QortalMetadata; data: any }[] = [];
|
||||||
language: sub.language,
|
for (const sub of subtitles) {
|
||||||
filename: sub.filename,
|
const data = {
|
||||||
type: sub.type
|
subtitleData: sub.base64,
|
||||||
}
|
language: sub.language,
|
||||||
|
filename: sub.filename,
|
||||||
const base64Data = await objectToBase64(data)
|
type: sub.type,
|
||||||
const resource = {
|
};
|
||||||
name,
|
|
||||||
identifier,
|
|
||||||
service: SERVICE_SUBTITLE,
|
|
||||||
base64: base64Data,
|
|
||||||
filename: sub.filename,
|
|
||||||
title: sub.language || undefined
|
|
||||||
}
|
|
||||||
resources.push(resource)
|
|
||||||
tempResources.push({
|
|
||||||
qortalMetadata: {
|
|
||||||
identifier,
|
|
||||||
service: SERVICE_SUBTITLE,
|
|
||||||
name,
|
|
||||||
size: 100,
|
|
||||||
created: Date.now()
|
|
||||||
},
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.log('resources', resources)
|
|
||||||
|
|
||||||
await qortalRequest({
|
const base64Data = await objectToBase64(data);
|
||||||
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
|
const resource = {
|
||||||
resources
|
name,
|
||||||
})
|
identifier,
|
||||||
|
service: SERVICE_SUBTITLE,
|
||||||
|
base64: base64Data,
|
||||||
|
filename: sub.filename,
|
||||||
|
title: sub.language || undefined,
|
||||||
|
};
|
||||||
|
resources.push(resource);
|
||||||
|
tempResources.push({
|
||||||
|
qortalMetadata: {
|
||||||
|
identifier,
|
||||||
|
service: SERVICE_SUBTITLE,
|
||||||
|
name,
|
||||||
|
size: 100,
|
||||||
|
created: Date.now(),
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("resources", resources);
|
||||||
|
|
||||||
|
await qortalRequest({
|
||||||
lists.addNewResources(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources)
|
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
|
||||||
} catch (error) {
|
resources,
|
||||||
|
});
|
||||||
}
|
|
||||||
|
lists.addNewResources(
|
||||||
|
`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`,
|
||||||
|
tempResources
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
const onBack = () => {
|
||||||
|
if (mode === 1) close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectHandler = (sub: SubtitlePublishedData) => {
|
||||||
|
console.log('onSelectHandler')
|
||||||
|
onSelect(sub);
|
||||||
|
close();
|
||||||
};
|
};
|
||||||
const onBack = ()=> {
|
|
||||||
if(mode === 1) close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSelectHandler = (sub: SubtitlePublishedData)=> {
|
|
||||||
onSelect(sub)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={!!open}
|
open={!!open}
|
||||||
anchorEl={subtitleBtnRef.current}
|
anchorEl={subtitleBtnRef.current}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
slots={{
|
slots={{
|
||||||
transition: Fade,
|
transition: Fade,
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
transition: {
|
transition: {
|
||||||
timeout: 200,
|
timeout: 200,
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
bgcolor: alpha("#181818", 0.98),
|
||||||
|
color: "white",
|
||||||
|
opacity: 0.9,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 5,
|
||||||
|
p: 1,
|
||||||
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
paper: {
|
},
|
||||||
sx: {
|
}}
|
||||||
bgcolor: alpha('#181818', 0.98),
|
anchorOrigin={{
|
||||||
color: 'white',
|
vertical: "top",
|
||||||
opacity: 0.9,
|
horizontal: "center",
|
||||||
borderRadius: 2,
|
}}
|
||||||
boxShadow: 5,
|
transformOrigin={{
|
||||||
p: 1,
|
vertical: "bottom",
|
||||||
minWidth: 200,
|
horizontal: "center",
|
||||||
},
|
}}
|
||||||
},
|
>
|
||||||
}}
|
<Box
|
||||||
anchorOrigin={{
|
sx={{
|
||||||
vertical: 'top',
|
padding: "5px 0px 10px 0px",
|
||||||
horizontal: 'center',
|
display: "flex",
|
||||||
}}
|
gap: "10px",
|
||||||
transformOrigin={{
|
width: "100%",
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{
|
<ButtonBase onClick={onBack}>
|
||||||
padding: '5px 0px 10px 0px',
|
<ArrowBackIosIcon
|
||||||
display: 'flex',
|
sx={{
|
||||||
gap:'10px',
|
fontSize: "1.15em",
|
||||||
width: '100%'
|
}}
|
||||||
}}>
|
/>
|
||||||
<ButtonBase onClick={onBack}>
|
</ButtonBase>
|
||||||
<ArrowBackIosIcon sx={{
|
<ButtonBase>
|
||||||
fontSize: '1.15em'
|
<Typography
|
||||||
}}/>
|
onClick={onBack}
|
||||||
</ButtonBase>
|
sx={{
|
||||||
<ButtonBase>
|
fontSize: "0.85rem",
|
||||||
<Typography onClick={onBack} sx={{
|
}}
|
||||||
fontSize: '0.85rem'
|
>
|
||||||
}}>Subtitles</Typography>
|
Subtitles
|
||||||
|
</Typography>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<ButtonBase sx={{
|
<ButtonBase
|
||||||
marginLeft: 'auto',
|
sx={{
|
||||||
|
marginLeft: "auto",
|
||||||
}}>
|
}}
|
||||||
<ModeEditIcon sx={{
|
>
|
||||||
fontSize: '1.15rem'
|
<ModeEditIcon
|
||||||
}} />
|
sx={{
|
||||||
</ButtonBase>
|
fontSize: "1.15rem",
|
||||||
</Box>
|
}}
|
||||||
<Divider />
|
/>
|
||||||
{mode === 1 && (
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
{mode === 1 && (
|
||||||
<PublisherSubtitles
|
<PublisherSubtitles
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
publisherName={qortalMetadata.name}
|
publisherName={qortalMetadata.name}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
onSelect={onSelectHandler}
|
onSelect={onSelectHandler}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
currentSubTrack={currentSubTrack}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* <Box>
|
{/* <Box>
|
||||||
{[
|
{[
|
||||||
'Ambient mode',
|
'Ambient mode',
|
||||||
'Annotations',
|
'Annotations',
|
||||||
@ -274,7 +303,7 @@ console.log('identifier2', identifier)
|
|||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</Box> */}
|
</Box> */}
|
||||||
</Popover>
|
</Popover>
|
||||||
// <Dialog
|
// <Dialog
|
||||||
// open={!!open}
|
// open={!!open}
|
||||||
// fullWidth={true}
|
// fullWidth={true}
|
||||||
@ -344,7 +373,8 @@ interface PublisherSubtitlesProps {
|
|||||||
subtitles: any[];
|
subtitles: any[];
|
||||||
setMode: (val: number) => void;
|
setMode: (val: number) => void;
|
||||||
onSelect: (subtitle: any) => void;
|
onSelect: (subtitle: any) => void;
|
||||||
onBack: ()=> void;
|
onBack: () => void;
|
||||||
|
currentSubTrack: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublisherSubtitles = ({
|
const PublisherSubtitles = ({
|
||||||
@ -352,28 +382,29 @@ const PublisherSubtitles = ({
|
|||||||
subtitles,
|
subtitles,
|
||||||
setMode,
|
setMode,
|
||||||
onSelect,
|
onSelect,
|
||||||
onBack
|
onBack,
|
||||||
|
currentSubTrack
|
||||||
}: PublisherSubtitlesProps) => {
|
}: PublisherSubtitlesProps) => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{subtitles?.map((sub) => {
|
||||||
{subtitles?.map((sub)=> {
|
return (
|
||||||
return <Subtitle onSelect={onSelect} sub={sub} key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}/>
|
<Subtitle
|
||||||
})}
|
currentSubtrack={currentSubTrack}
|
||||||
|
onSelect={onSelect}
|
||||||
|
sub={sub}
|
||||||
|
key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PublishSubtitlesProps {
|
interface PublishSubtitlesProps {
|
||||||
publishHandler: (subs: Subtitle[])=> void
|
publishHandler: (subs: Subtitle[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
|
const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
|
||||||
const [language, setLanguage] = useState<null | string>(null);
|
const [language, setLanguage] = useState<null | string>(null);
|
||||||
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
|
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
|
||||||
@ -388,7 +419,7 @@ const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
|
|||||||
filename: file.name,
|
filename: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
};
|
};
|
||||||
newSubtitles.push(newSubtitle)
|
newSubtitles.push(newSubtitle);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse audio file:", error);
|
console.error("Failed to parse audio file:", error);
|
||||||
}
|
}
|
||||||
@ -412,19 +443,19 @@ const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
|
|||||||
maxSize: 2 * 1024 * 1024, // 2MB
|
maxSize: 2 * 1024 * 1024, // 2MB
|
||||||
});
|
});
|
||||||
|
|
||||||
const onChangeValue = (field: string, data: any, index: number) => {
|
const onChangeValue = (field: string, data: any, index: number) => {
|
||||||
const sub = subtitles[index];
|
const sub = subtitles[index];
|
||||||
if (!sub) return;
|
if (!sub) return;
|
||||||
|
|
||||||
const copySub = { ...sub, [field]: data };
|
const copySub = { ...sub, [field]: data };
|
||||||
|
|
||||||
setSubtitles((prev) => {
|
setSubtitles((prev) => {
|
||||||
const copyPrev = [...prev];
|
const copyPrev = [...prev];
|
||||||
copyPrev[index] = copySub;
|
copyPrev[index] = copySub;
|
||||||
return copyPrev;
|
return copyPrev;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
console.log('subtitles', subtitles)
|
console.log("subtitles", subtitles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -438,64 +469,78 @@ console.log('subtitles', subtitles)
|
|||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box {...getRootProps()}>
|
<Box {...getRootProps()}>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
gap: '10px',
|
gap: "10px",
|
||||||
}}
|
}}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
Import subtitles
|
Import subtitles
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{subtitles?.map((sub, i) => {
|
{subtitles?.map((sub, i) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LanguageSelect
|
<LanguageSelect
|
||||||
value={sub.language}
|
value={sub.language}
|
||||||
onChange={(val: string | null) => onChangeValue('language',val, i)}
|
onChange={(val: string | null) =>
|
||||||
|
onChangeValue("language", val, i)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
onClick={()=> publishHandler(subtitles)}
|
onClick={() => publishHandler(subtitles)}
|
||||||
// disabled={disableButton}
|
// disabled={disableButton}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
Publish index
|
Publish index
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SubProps {
|
interface SubProps {
|
||||||
sub: QortalGetMetadata
|
sub: QortalGetMetadata;
|
||||||
onSelect: (subtitle: Subtitle)=> void;
|
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={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{resource?.data?.language}
|
|
||||||
</Typography>
|
|
||||||
}
|
}
|
||||||
|
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)",
|
||||||
|
},
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}>
|
||||||
|
<Typography
|
||||||
|
|
||||||
|
|
||||||
|
>
|
||||||
|
{resource?.data?.language}
|
||||||
|
</Typography>
|
||||||
|
{isSelected ? (
|
||||||
|
<CheckIcon />
|
||||||
|
) : (
|
||||||
|
<ArrowForwardIosIcon />
|
||||||
|
)}
|
||||||
|
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SubtitleManager = React.memo(SubtitleManagerComponent);
|
export const SubtitleManager = React.memo(SubtitleManagerComponent);
|
||||||
|
@ -65,6 +65,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
|||||||
opacity: showControls ? 1 : 0,
|
opacity: showControls ? 1 : 0,
|
||||||
pointerEvents: showControls ? 'auto' : 'none',
|
pointerEvents: showControls ? 'auto' : 'none',
|
||||||
transition: 'opacity 0.4s ease-in-out',
|
transition: 'opacity 0.4s ease-in-out',
|
||||||
|
width: '100%'
|
||||||
// ...additionalStyles
|
// ...additionalStyles
|
||||||
// height: controlsHeight,
|
// height: controlsHeight,
|
||||||
}}
|
}}
|
||||||
@ -94,7 +95,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
|||||||
<VideoTime progress={progress} duration={duration}/>
|
<VideoTime progress={progress} duration={duration}/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={controlGroupSX}>
|
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
|
||||||
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
||||||
<ObjectFitButton />
|
<ObjectFitButton />
|
||||||
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
|
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
|
||||||
|
@ -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 { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||||
import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
|
import { VideoContainer, VideoElement } from "./VideoPlayer-styles";
|
||||||
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
|
import { useVideoPlayerHotKeys } from "./useVideoPlayerHotKeys";
|
||||||
@ -6,15 +15,21 @@ import { useProgressStore, useVideoStore } from "../../state/video";
|
|||||||
import { useVideoPlayerController } from "./useVideoPlayerController";
|
import { useVideoPlayerController } from "./useVideoPlayerController";
|
||||||
import { LoadingVideo } from "./LoadingVideo";
|
import { LoadingVideo } from "./LoadingVideo";
|
||||||
import { VideoControlsBar } from "./VideoControlsBar";
|
import { VideoControlsBar } from "./VideoControlsBar";
|
||||||
import videojs from 'video.js';
|
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 { Subtitle, SubtitleManager, SubtitlePublishedData } from "./SubtitleManager";
|
import {
|
||||||
|
Subtitle,
|
||||||
|
SubtitleManager,
|
||||||
|
SubtitlePublishedData,
|
||||||
|
} from "./SubtitleManager";
|
||||||
import { base64ToBlobUrl } from "../../utils/base64";
|
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 {
|
try {
|
||||||
// Step 1: Convert base64 string to a Uint8Array
|
// Step 1: Convert base64 string to a Uint8Array
|
||||||
const binary = atob(base64Srt);
|
const binary = atob(base64Srt);
|
||||||
@ -24,20 +39,19 @@ export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise<string |
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Create a Blob from the Uint8Array with correct MIME type
|
// Step 2: Create a Blob from the Uint8Array with correct MIME type
|
||||||
const srtBlob = new Blob([bytes], { type: 'application/x-subrip' });
|
const srtBlob = new Blob([bytes], { type: "application/x-subrip" });
|
||||||
console.log('srtBlob', srtBlob)
|
console.log("srtBlob", srtBlob);
|
||||||
// Step 3: Use convert() with the Blob
|
// Step 3: Use convert() with the Blob
|
||||||
const vttBlobUrl: string = await convert(srtBlob);
|
const vttBlobUrl: string = await convert(srtBlob);
|
||||||
return vttBlobUrl
|
return vttBlobUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to convert SRT to VTT:', error);
|
console.error("Failed to convert SRT to VTT:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
||||||
|
|
||||||
|
interface VideoPlayerProps {
|
||||||
interface VideoPlayerProps {
|
|
||||||
qortalVideoResource: QortalGetMetadata;
|
qortalVideoResource: QortalGetMetadata;
|
||||||
videoRef: Ref<HTMLVideoElement>;
|
videoRef: Ref<HTMLVideoElement>;
|
||||||
retryAttempts?: number;
|
retryAttempts?: number;
|
||||||
@ -47,27 +61,30 @@ type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
|||||||
}
|
}
|
||||||
|
|
||||||
const videoStyles = {
|
const videoStyles = {
|
||||||
videoContainer: { },
|
videoContainer: {},
|
||||||
video: { },
|
video: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadMediaInfo(wasmPath = '/MediaInfoModule.wasm') {
|
async function loadMediaInfo(wasmPath = "/MediaInfoModule.wasm") {
|
||||||
const mediaInfoModule = await import('mediainfo.js');
|
const mediaInfoModule = await import("mediainfo.js");
|
||||||
return await mediaInfoModule.default({
|
return await mediaInfoModule.default({
|
||||||
format: 'JSON',
|
format: "JSON",
|
||||||
full: true,
|
full: true,
|
||||||
locateFile: () => wasmPath,
|
locateFile: () => wasmPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoMimeTypeFromUrl(qortalVideoResource: any): Promise<string | null> {
|
async function getVideoMimeTypeFromUrl(
|
||||||
|
qortalVideoResource: any
|
||||||
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const metadataResponse = await fetch(`/arbitrary/metadata/${qortalVideoResource.service}/${qortalVideoResource.name}/${qortalVideoResource.identifier}`)
|
const metadataResponse = await fetch(
|
||||||
const metadataData = await metadataResponse.json()
|
`/arbitrary/metadata/${qortalVideoResource.service}/${qortalVideoResource.name}/${qortalVideoResource.identifier}`
|
||||||
return metadataData?.mimeType || null
|
);
|
||||||
|
const metadataData = await metadataResponse.json();
|
||||||
|
return metadataData?.mimeType || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
// const mediaInfo = await loadMediaInfo();
|
// const mediaInfo = await loadMediaInfo();
|
||||||
// const chunkCache = new Map<string, Uint8Array>();
|
// const chunkCache = new Map<string, Uint8Array>();
|
||||||
@ -147,23 +164,26 @@ 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, setPlaybackRate, playbackRate } = useVideoStore((state) => ({
|
const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore(
|
||||||
volume: state.playbackSettings.volume,
|
(state) => ({
|
||||||
setVolume: state.setVolume,
|
volume: state.playbackSettings.volume,
|
||||||
setPlaybackRate: state.setPlaybackRate,
|
setVolume: state.setVolume,
|
||||||
playbackRate: state.playbackSettings.playbackRate
|
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 [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();
|
||||||
const [localProgress, setLocalProgress] = useState(0)
|
const [localProgress, setLocalProgress] = useState(0);
|
||||||
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 [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
|
||||||
const subtitleBtnRef = useRef(null)
|
const subtitleBtnRef = useRef(null);
|
||||||
|
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
|
||||||
const {
|
const {
|
||||||
reloadVideo,
|
reloadVideo,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
@ -183,15 +203,15 @@ export const VideoPlayer = ({
|
|||||||
startPlay,
|
startPlay,
|
||||||
setProgressAbsolute,
|
setProgressAbsolute,
|
||||||
setAlwaysShowControls,
|
setAlwaysShowControls,
|
||||||
status, percentLoaded,
|
status,
|
||||||
|
percentLoaded,
|
||||||
showControlsFullScreen,
|
showControlsFullScreen,
|
||||||
|
|
||||||
} = useVideoPlayerController({
|
} = useVideoPlayerController({
|
||||||
autoPlay,
|
autoPlay,
|
||||||
playerRef,
|
playerRef,
|
||||||
qortalVideoResource,
|
qortalVideoResource,
|
||||||
retryAttempts,
|
retryAttempts,
|
||||||
isPlayerInitialized
|
isPlayerInitialized,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hotkeyHandlers = useMemo(
|
const hotkeyHandlers = useMemo(
|
||||||
@ -223,16 +243,12 @@ export const VideoPlayer = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeSubtitleManager = useCallback(() => {
|
||||||
|
setIsOpenSubtitleManage(false);
|
||||||
|
}, []);
|
||||||
|
const openSubtitleManager = useCallback(() => {
|
||||||
const closeSubtitleManager = useCallback(()=> {
|
setIsOpenSubtitleManage(true);
|
||||||
setIsOpenSubtitleManage(false)
|
}, []);
|
||||||
}, [])
|
|
||||||
const openSubtitleManager = useCallback(()=> {
|
|
||||||
setIsOpenSubtitleManage(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const videoLocation = useMemo(() => {
|
const videoLocation = useMemo(() => {
|
||||||
if (!qortalVideoResource) return null;
|
if (!qortalVideoResource) return null;
|
||||||
@ -241,15 +257,15 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
useVideoPlayerHotKeys(hotkeyHandlers);
|
useVideoPlayerHotKeys(hotkeyHandlers);
|
||||||
|
|
||||||
const updateProgress = useCallback(() => {
|
const updateProgress = useCallback(() => {
|
||||||
const player = playerRef?.current;
|
const player = playerRef?.current;
|
||||||
if (!player || typeof player?.currentTime !== 'function') return;
|
if (!player || typeof player?.currentTime !== "function") return;
|
||||||
|
|
||||||
const currentTime = player.currentTime();
|
const currentTime = player.currentTime();
|
||||||
if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
|
if (typeof currentTime === "number" && videoLocation && currentTime > 0.1) {
|
||||||
setProgress(videoLocation, currentTime);
|
setProgress(videoLocation, currentTime);
|
||||||
setLocalProgress(currentTime);
|
setLocalProgress(currentTime);
|
||||||
}
|
}
|
||||||
}, [videoLocation]);
|
}, [videoLocation]);
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
||||||
// if (!ref.current) return;
|
// if (!ref.current) return;
|
||||||
@ -271,33 +287,30 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
||||||
try {
|
try {
|
||||||
const video = e.currentTarget;
|
const video = e.currentTarget;
|
||||||
setVolume(video.volume);
|
setVolume(video.volume);
|
||||||
setIsMuted(video.muted);
|
setIsMuted(video.muted);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('onVolumeChangeHandler', onVolumeChangeHandler)
|
console.error("onVolumeChangeHandler", onVolumeChangeHandler);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setIsMuted, setVolume]
|
[setIsMuted, setVolume]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const videoStylesContainer = useMemo(() => {
|
const videoStylesContainer = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
cursor: showControls ? 'auto' : 'none',
|
cursor: showControls ? "auto" : "none",
|
||||||
aspectRatio: '16 / 9',
|
aspectRatio: "16 / 9",
|
||||||
...videoStyles?.videoContainer,
|
...videoStyles?.videoContainer,
|
||||||
};
|
};
|
||||||
}, [showControls]);
|
}, [showControls]);
|
||||||
|
|
||||||
|
|
||||||
const videoStylesVideo = useMemo(() => {
|
const videoStylesVideo = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...videoStyles?.video,
|
...videoStyles?.video,
|
||||||
objectFit: videoObjectFit,
|
objectFit: videoObjectFit,
|
||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
height: isFullscreen ? "calc(100vh - 40px)" : "100%",
|
height: isFullscreen ? "calc(100vh - 40px)" : "100%",
|
||||||
width: '100%'
|
width: "100%",
|
||||||
};
|
};
|
||||||
}, [videoObjectFit, isFullscreen]);
|
}, [videoObjectFit, isFullscreen]);
|
||||||
|
|
||||||
@ -310,29 +323,28 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
[onEnded]
|
[onEnded]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCanPlay = useCallback(()=> {
|
const handleCanPlay = useCallback(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [setIsLoading])
|
}, [setIsLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!isPlayerInitialized) return
|
if (!isPlayerInitialized) return;
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
if (!player || typeof player.on !== 'function') return;
|
if (!player || typeof player.on !== "function") return;
|
||||||
|
|
||||||
const handleLoadedMetadata = () => {
|
const handleLoadedMetadata = () => {
|
||||||
const duration = player.duration?.();
|
const duration = player.duration?.();
|
||||||
if (typeof duration === 'number' && !isNaN(duration)) {
|
if (typeof duration === "number" && !isNaN(duration)) {
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
player.on('loadedmetadata', handleLoadedMetadata);
|
player.on("loadedmetadata", handleLoadedMetadata);
|
||||||
|
|
||||||
return () => {
|
|
||||||
player.off('loadedmetadata', handleLoadedMetadata);
|
|
||||||
};
|
|
||||||
}, [isPlayerInitialized]);
|
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
player.off("loadedmetadata", handleLoadedMetadata);
|
||||||
|
};
|
||||||
|
}, [isPlayerInitialized]);
|
||||||
|
|
||||||
const enterFullscreen = () => {
|
const enterFullscreen = () => {
|
||||||
const ref = containerRef?.current as any;
|
const ref = containerRef?.current as any;
|
||||||
@ -341,8 +353,6 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
if (ref.requestFullscreen && !isFullscreen) {
|
if (ref.requestFullscreen && !isFullscreen) {
|
||||||
ref.requestFullscreen();
|
ref.requestFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const exitFullscreen = () => {
|
const exitFullscreen = () => {
|
||||||
@ -353,303 +363,397 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
isFullscreen ? exitFullscreen() : enterFullscreen();
|
isFullscreen ? exitFullscreen() : enterFullscreen();
|
||||||
};
|
};
|
||||||
|
|
||||||
const canvasRef = useRef(null)
|
const canvasRef = useRef(null);
|
||||||
const videoRefForCanvas = useRef<any>(null)
|
const videoRefForCanvas = useRef<any>(null);
|
||||||
const extractFrames = useCallback( (time: number): void => {
|
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
|
||||||
|
// if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
|
||||||
// // Avoid unnecessary resize if already correct
|
// canvas.width = video.videoWidth;
|
||||||
// if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
|
// 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 context = canvas.getContext("2d");
|
// const threshold = 0.01; // 10ms threshold
|
||||||
// if (!context) return null;
|
// if (Math.abs(video.currentTime - time) > threshold) {
|
||||||
|
// await new Promise<void>((resolve) => {
|
||||||
// // If video is already near the correct time, don't seek again
|
// const onSeeked = () => resolve();
|
||||||
// const threshold = 0.01; // 10ms threshold
|
// video.addEventListener("seeked", onSeeked, { once: true });
|
||||||
// if (Math.abs(video.currentTime - time) > threshold) {
|
// video.currentTime = time;
|
||||||
// await new Promise<void>((resolve) => {
|
// });
|
||||||
// const onSeeked = () => resolve();
|
// }
|
||||||
// video.addEventListener("seeked", onSeeked, { once: true });
|
// context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
// video.currentTime = time;
|
// // 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);
|
||||||
|
// });
|
||||||
// context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
// if (!blob) return null;
|
||||||
|
// return URL.createObjectURL(blob);
|
||||||
// // 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 hideTimeout = useRef<any>(null);
|
||||||
|
|
||||||
|
|
||||||
const resetHideTimer = () => {
|
const resetHideTimer = () => {
|
||||||
setShowControls(true);
|
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);
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
||||||
|
hideTimeout.current = setTimeout(() => {
|
||||||
|
setShowControls(false);
|
||||||
|
}, 2500); // 3s of inactivity
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
const previousSubtitleUrlRef = useRef<string | null>(null);
|
const handleMouseMove = () => {
|
||||||
|
resetHideTimer();
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
// Component unmount cleanup
|
|
||||||
if (previousSubtitleUrlRef.current) {
|
|
||||||
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
|
||||||
previousSubtitleUrlRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSelectSubtitle = useCallback(async (subtitle: SubtitlePublishedData)=> {
|
useEffect(() => {
|
||||||
console.log('onSelectSubtitle', subtitle)
|
resetHideTimer(); // initial show
|
||||||
const player = playerRef.current;
|
return () => {
|
||||||
if (!player || !subtitle.subtitleData || !subtitle.type) return;
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Cleanup: revoke previous Blob URL
|
const previousSubtitleUrlRef = useRef<string | null>(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)
|
|
||||||
|
|
||||||
}
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
previousSubtitleUrlRef.current = blobUrl;
|
// Component unmount cleanup
|
||||||
|
if (previousSubtitleUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
||||||
|
previousSubtitleUrlRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const remoteTracksList = playerRef.current?.remoteTextTracks();
|
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) {
|
if (remoteTracksList) {
|
||||||
const toRemove: TextTrack[] = [];
|
const toRemove: TextTrack[] = [];
|
||||||
|
|
||||||
// Bypass TS restrictions safely
|
// 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++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
const track = list[i];
|
const track = list[i];
|
||||||
if (track) toRemove.push(track);
|
if (track) toRemove.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemove.forEach((track) => {
|
toRemove.forEach((track) => {
|
||||||
playerRef.current?.removeRemoteTextTrack(track);
|
playerRef.current?.removeRemoteTextTrack(track);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
playerRef.current?.addRemoteTextTrack({
|
|
||||||
kind: 'subtitles',
|
|
||||||
src: blobUrl,
|
|
||||||
srclang: 'en',
|
|
||||||
label: 'English',
|
|
||||||
default: true
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Remove all existing remote text tracks
|
return
|
||||||
// try {
|
}
|
||||||
// const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_
|
console.log("onSelectSubtitle", subtitle);
|
||||||
// if (remoteTracks && remoteTracks?.length) {
|
const player = playerRef.current;
|
||||||
// const toRemove: TextTrack[] = [];
|
if (!player || !subtitle.subtitleData || !subtitle.type) return;
|
||||||
// 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)=> {
|
// Cleanup: revoke previous Blob URL
|
||||||
setTimeout(() => {
|
if (previousSubtitleUrlRef.current) {
|
||||||
res(null)
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
||||||
}, 1000);
|
previousSubtitleUrlRef.current = null;
|
||||||
})
|
}
|
||||||
const tracksInfo = playerRef.current?.textTracks();
|
let blobUrl;
|
||||||
console.log('tracksInfo', tracksInfo)
|
if (subtitle?.type === "application/x-subrip") {
|
||||||
if (!tracksInfo) return;
|
blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData);
|
||||||
|
} else {
|
||||||
|
blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type);
|
||||||
|
}
|
||||||
|
|
||||||
const tracks = Array.from({ length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i]);
|
previousSubtitleUrlRef.current = blobUrl;
|
||||||
console.log('tracks', tracks)
|
|
||||||
for (const track of tracks) {
|
|
||||||
console.log('track', track)
|
|
||||||
|
|
||||||
if (track.kind === 'subtitles') {
|
const remoteTracksList = playerRef.current?.remoteTextTracks();
|
||||||
track.mode = 'showing'; // force display
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setShowControls(false);
|
setShowControls(false);
|
||||||
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
if (hideTimeout.current) clearTimeout(hideTimeout.current);
|
||||||
}, [setShowControls]);
|
}, [setShowControls]);
|
||||||
|
|
||||||
|
const videoLocactionStringified = useMemo(() => {
|
||||||
const videoLocactionStringified = useMemo(()=> {
|
return JSON.stringify(qortalVideoResource);
|
||||||
return JSON.stringify(qortalVideoResource)
|
}, [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();
|
|
||||||
|
|
||||||
});
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if(!isPlayerInitialized) return
|
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
|
||||||
const player = playerRef?.current;
|
return;
|
||||||
if (!player) return;
|
|
||||||
|
|
||||||
const handleRateChange = () => {
|
const resource = JSON.parse(videoLocactionStringified);
|
||||||
const newRate = player?.playbackRate();
|
let canceled = false;
|
||||||
if(newRate){
|
|
||||||
setPlaybackRate(newRate); // or any other state/action
|
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;
|
||||||
|
|
||||||
player.on('ratechange', handleRateChange);
|
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]);
|
||||||
|
|
||||||
return () => {
|
useEffect(() => {
|
||||||
player.off('ratechange', handleRateChange);
|
if (!isPlayerInitialized) return;
|
||||||
};
|
const player = playerRef?.current;
|
||||||
}, [isPlayerInitialized]);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <video controls src={"http://127.0.0.1:22393/arbitrary/VIDEO/a-test/MYTEST2_like_MYTEST2_vid_test-parallel_cSYmIk"} ref={videoRefForCanvas} ></video> */}
|
{/* <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}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<LoadingVideo togglePlay={togglePlay} isReady={isReady} status={status} percentLoaded={percentLoaded} isLoading={isLoading} />
|
<LoadingVideo
|
||||||
<VideoElement
|
togglePlay={togglePlay}
|
||||||
ref={videoRef}
|
isReady={isReady}
|
||||||
tabIndex={0}
|
status={status}
|
||||||
className="video-js"
|
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> */}
|
||||||
|
|
||||||
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 && (
|
{isReady && (
|
||||||
<VideoControlsBar subtitleBtnRef={subtitleBtnRef} playbackRate={playbackRate} increaseSpeed={hotkeyHandlers.increaseSpeed}
|
<VideoControlsBar
|
||||||
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} />
|
subtitleBtnRef={subtitleBtnRef}
|
||||||
)}
|
playbackRate={playbackRate}
|
||||||
|
increaseSpeed={hotkeyHandlers.increaseSpeed}
|
||||||
<SubtitleManager subtitleBtnRef={subtitleBtnRef} close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} onSelect={onSelectSubtitle} />
|
decreaseSpeed={hotkeyHandlers.decreaseSpeed}
|
||||||
</VideoContainer>
|
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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user