applied subtitles vtt and srt

This commit is contained in:
PhilReact 2025-06-15 07:00:27 +03:00
parent b48b47ae1c
commit d3c4a4713e
15 changed files with 669 additions and 193 deletions

10
package-lock.json generated
View File

@ -18,6 +18,7 @@
"dexie": "^4.0.11",
"dompurify": "^3.2.4",
"idb-keyval": "^6.2.2",
"iso-639-1": "^3.1.5",
"react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2",
"react-idle-timer": "^5.7.2",
@ -2566,6 +2567,15 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/iso-639-1": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz",
"integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",

View File

@ -32,6 +32,7 @@
"dexie": "^4.0.11",
"dompurify": "^3.2.4",
"idb-keyval": "^6.2.2",
"iso-639-1": "^3.1.5",
"react-dropzone": "^14.3.8",
"react-hot-toast": "^2.5.2",
"react-idle-timer": "^5.7.2",

View File

@ -0,0 +1,33 @@
// components/LanguageSelector.tsx
import React from 'react';
import { Autocomplete, TextField } from '@mui/material';
import { languageOptions } from './SubtitleManager';
export default function LanguageSelector({
value,
onChange,
}: {
value: string | null;
onChange: (value: string | null) => void;
}) {
return (
<Autocomplete
options={languageOptions}
getOptionLabel={(option) => `${option.name} (${option.code})`}
value={languageOptions.find((opt) => opt.code === value) || null}
onChange={(event, newValue) => onChange(newValue?.code || null)}
renderInput={(params) => <TextField {...params} label="Subtitle Language" />}
isOptionEqualToValue={(option, val) => option.code === val.code}
sx={{ width: 300 }}
slotProps={{
popper: {
sx: {
zIndex: 999991, // Must be higher than Dialog's default zIndex (1300)
},
},
}}
/>
);
}

View File

@ -1,117 +1,350 @@
import { useCallback, useEffect, useState } from "react"
import { QortalGetMetadata } from "../../types/interfaces/resources"
import { Box, ButtonBase, Dialog, DialogContent, DialogTitle, IconButton, Typography } from "@mui/material"
import React, { useCallback, useEffect, useState } from "react";
import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources";
import {
alpha,
Box,
Button,
ButtonBase,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Fade,
IconButton,
Popover,
Typography,
} from "@mui/material";
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 { useResources } from "../../hooks/useResources";
import { Resource, useResources } from "../../hooks/useResources";
import { useGlobal } from "../../context/GlobalProvider";
import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants";
import ISO6391, { LanguageCode } from "iso-639-1";
import LanguageSelect from "./LanguageSelect";
import {
useDropzone,
DropzoneRootProps,
DropzoneInputProps,
} from "react-dropzone";
import { fileToBase64, objectToBase64 } from "../../utils/base64";
import { ResourceToPublish } from "../../types/qortalRequests/types";
import { useListReturn } from "../../hooks/useListData";
import { usePublish } from "../../hooks/usePublish";
interface SubtitleManagerProps {
qortalMetadata: QortalGetMetadata
close: ()=> void
open: boolean
qortalMetadata: QortalGetMetadata;
close: () => void;
open: boolean;
onSelect: (subtitle: SubtitlePublishedData)=> void;
subtitleBtnRef: any
}
export interface Subtitle {
language: string | null;
base64: string;
type: string;
filename: string;
size: number;
}
export interface SubtitlePublishedData {
language: string | null;
subtitleData: string;
type: string;
filename: string;
size: number;
}
export const SubtitleManager = ({qortalMetadata, open, close}: SubtitleManagerProps) => {
const [mode, setMode] = useState(1)
const {lists} = useGlobal()
const {fetchResources} = useResources()
// const [subtitles, setSubtitles] = useState([])
const subtitles = useListStore(
(state) => state.lists[`${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`]?.items || []
);
const getPublishedSubtitles = useCallback(async ()=> {
try {
await fetchResources(qortalMetadata, `${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`, "BASE64");
} catch (error) {
console.error(error)
}
}, [])
useEffect(()=> {
if(!qortalMetadata?.identifier || !qortalMetadata?.name || !qortalMetadata?.service) return
export const languageOptions = ISO6391.getAllCodes().map((code) => ({
code,
name: ISO6391.getName(code),
nativeName: ISO6391.getNativeName(code),
}));
const SubtitleManagerComponent = ({
qortalMetadata,
open,
close,
onSelect,
subtitleBtnRef
}: 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}`)
// getPublishedSubtitles()
}, [qortalMetadata?.identifier, qortalMetadata?.service, qortalMetadata?.name, getPublishedSubtitles])
console.log('subtitles222', subtitles)
const getPublishedSubtitles = useCallback(async () => {
try {
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
console.log('videoId', videoId)
const postIdSearch = await identifierOperations.buildSearchPrefix(
ENTITY_SUBTITLE,
videoId,
);
const searchParams = {
service: SERVICE_SUBTITLE,
identifier: postIdSearch,
limit: 0
};
const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64");
lists.addList(`subs-${videoId}`, res || []);
console.log('resres2', res)
} catch (error) {
console.error(error);
}
}, []);
const handleClose = () => {
close()
useEffect(() => {
if (
!qortalMetadata?.identifier ||
!qortalMetadata?.name ||
!qortalMetadata?.service
)
return;
getPublishedSubtitles()
}, [
qortalMetadata?.identifier,
qortalMetadata?.service,
qortalMetadata?.name,
getPublishedSubtitles,
]);
const handleClose = () => {
close();
setMode(1);
// setTitle("");
// setDescription("");
// setHasMetadata(false);
};
const onSelect = ()=> {
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}[] = []
for(const sub of subtitles ){
const data = {
subtitleData: sub.base64,
language: sub.language,
filename: sub.filename,
type: sub.type
}
const base64Data = await objectToBase64(data)
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({
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
resources
})
lists.addNewResources(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources)
} catch (error) {
}
};
const onBack = ()=> {
if(mode === 1) close()
}
const onSelectHandler = (sub: SubtitlePublishedData)=> {
onSelect(sub)
close()
}
return (
<Dialog
open={!!open}
fullWidth={true}
maxWidth={"md"}
sx={{
zIndex: 999990,
}}
slotProps={{
paper: {
elevation: 0,
},
}}
>
<DialogTitle>Subtitles</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
})}
<Popover
open={!!open}
anchorEl={subtitleBtnRef.current}
onClose={handleClose}
slots={{
transition: Fade,
}}
slotProps={{
transition: {
timeout: 200,
},
paper: {
sx: {
bgcolor: alpha('#181818', 0.98),
color: 'white',
opacity: 0.9,
borderRadius: 2,
boxShadow: 5,
p: 1,
minWidth: 200,
},
},
}}
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
>
<CloseIcon />
</IconButton>
{mode === 1 && (
<Box sx={{
padding: '5px 0px 10px 0px',
display: 'flex',
gap:'10px',
width: '100%'
}}>
<ButtonBase onClick={onBack}>
<ArrowBackIosIcon sx={{
fontSize: '1.15em'
}}/>
</ButtonBase>
<ButtonBase>
<Typography onClick={onBack} sx={{
fontSize: '0.85rem'
}}>Subtitles</Typography>
</ButtonBase>
<ButtonBase sx={{
marginLeft: 'auto',
}}>
<ModeEditIcon sx={{
fontSize: '1.15rem'
}} />
</ButtonBase>
</Box>
<Divider />
{mode === 1 && (
<PublisherSubtitles
subtitles={subtitles}
publisherName={qortalMetadata.name}
setMode={setMode}
onSelect={onSelect}
/>
)}
{/* {mode === 2 && (
<CommunitySubtitles
link={open?.link}
name={open?.name}
mode={mode}
setMode={setMode}
username={username}
category={open?.category}
rootName={open?.rootName}
onSelect={onSelectHandler}
onBack={onBack}
/>
)}
{/* <Box>
{[
'Ambient mode',
'Annotations',
'Subtitles/CC',
'Sleep timer',
'Playback speed',
'Quality',
].map((label) => (
<Typography
key={label}
sx={{
px: 2,
py: 1,
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
},
}}
>
{label}
</Typography>
))}
</Box> */}
</Popover>
// <Dialog
// open={!!open}
// fullWidth={true}
// maxWidth={"md"}
// sx={{
// zIndex: 999990,
// }}
// slotProps={{
// paper: {
// elevation: 0,
// },
// }}
// >
// <DialogTitle>Subtitles</DialogTitle>
// <IconButton
// aria-label="close"
// onClick={handleClose}
// sx={(theme) => ({
// position: "absolute",
// right: 8,
// top: 8,
// })}
// >
// <CloseIcon />
// </IconButton>
// <Button onClick={() => setMode(5)}>New subtitles</Button>
// {mode === 1 && (
// <PublisherSubtitles
// subtitles={subtitles}
// publisherName={qortalMetadata.name}
// setMode={setMode}
// onSelect={onSelect}
// />
// )}
// {mode === 5 && <PublishSubtitles publishHandler={publishHandler} />}
// {/* {mode === 2 && (
// <CommunitySubtitles
// link={open?.link}
// name={open?.name}
// mode={mode}
// setMode={setMode}
// username={username}
// category={open?.category}
// rootName={open?.rootName}
// />
// )}
{mode === 4 && (
<MySubtitles
link={open?.link}
name={open?.name}
mode={mode}
setMode={setMode}
username={username}
title={title}
description={description}
setDescription={setDescription}
setTitle={setTitle}
/>
)} */}
</Dialog>
)
}
// {mode === 4 && (
// <MySubtitles
// link={open?.link}
// name={open?.name}
// mode={mode}
// setMode={setMode}
// username={username}
// title={title}
// description={description}
// setDescription={setDescription}
// setTitle={setTitle}
// />
// )} */}
// </Dialog>
);
};
interface PublisherSubtitlesProps {
publisherName: string
subtitles: any[]
setMode: (val: number)=> void
onSelect: (subtitle: any)=> void
publisherName: string;
subtitles: any[];
setMode: (val: number) => void;
onSelect: (subtitle: any) => void;
onBack: ()=> void;
}
const PublisherSubtitles = ({
@ -119,8 +352,80 @@ const PublisherSubtitles = ({
subtitles,
setMode,
onSelect,
onBack
}: PublisherSubtitlesProps) => {
return (
<>
{subtitles?.map((sub)=> {
return <Subtitle onSelect={onSelect} sub={sub} key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}/>
})}
</>
);
};
interface PublishSubtitlesProps {
publishHandler: (subs: Subtitle[])=> void
}
const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
const [language, setLanguage] = useState<null | string>(null);
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
const onDrop = useCallback(async (acceptedFiles: File[]) => {
const newSubtitles: Subtitle[] = [];
for (const file of acceptedFiles) {
try {
const newSubtitle = {
base64: await fileToBase64(file),
language: null,
type: file.type,
filename: file.name,
size: file.size,
};
newSubtitles.push(newSubtitle)
} catch (error) {
console.error("Failed to parse audio file:", error);
}
}
setSubtitles((prev) => [...newSubtitles, ...prev]);
}, []);
const {
getRootProps,
getInputProps,
}: {
getRootProps: () => DropzoneRootProps;
getInputProps: () => DropzoneInputProps;
isDragActive: boolean;
} = useDropzone({
onDrop,
accept: {
"application/x-subrip": [".srt"], // SRT subtitles
"text/vtt": [".vtt"], // WebVTT subtitles
},
multiple: true,
maxSize: 2 * 1024 * 1024, // 2MB
});
const onChangeValue = (field: string, data: any, index: number) => {
const sub = subtitles[index];
if (!sub) return;
const copySub = { ...sub, [field]: data };
setSubtitles((prev) => {
const copyPrev = [...prev];
copyPrev[index] = copySub;
return copyPrev;
});
};
console.log('subtitles', subtitles)
return (
<>
<DialogContent>
@ -133,27 +438,64 @@ const PublisherSubtitles = ({
alignItems: "flex-start",
}}
>
<ButtonBase
<Box {...getRootProps()}>
<Button
sx={{
width: "100%",
display: 'flex',
gap: '10px',
}}
onClick={() => setMode(2)}
variant="contained"
>
<Box
sx={{
p: 2,
border: "2px solid",
borderRadius: 2,
width: "100%",
}}
>
<Typography>Create new index</Typography>
</Box>
</ButtonBase>
<input {...getInputProps()} />
Import subtitles
</Button>
</Box>
{subtitles?.map((sub, i) => {
return (
<>
<LanguageSelect
value={sub.language}
onChange={(val: string | null) => onChangeValue('language',val, i)}
/>
</>
);
})}
</Box>
</DialogContent>
<DialogActions>
<Button
onClick={()=> publishHandler(subtitles)}
// disabled={disableButton}
variant="contained"
>
Publish index
</Button>
</DialogActions>
</>
);
};
};
interface SubProps {
sub: QortalGetMetadata
onSelect: (subtitle: Subtitle)=> void;
}
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>
}
export const SubtitleManager = React.memo(SubtitleManagerComponent);

View File

@ -35,9 +35,10 @@ interface VideoControlsBarProps {
decreaseSpeed: ()=> void
playbackRate: number
openSubtitleManager: ()=> void
subtitleBtnRef: any
}
export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => {
export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => {
const showMobileControls = isScreenSmall && canPlay;
@ -96,7 +97,7 @@ export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decr
<Box sx={controlGroupSX}>
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton />
<IconButton onClick={openSubtitleManager}>
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
sub
</IconButton>
<PictureInPictureButton />

View File

@ -10,9 +10,30 @@ import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import Player from "video.js/dist/types/player";
import { SubtitleManager } from "./SubtitleManager";
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";
@ -142,6 +163,7 @@ export const VideoPlayer = ({
const [isLoading, setIsLoading] = useState(true);
const [showControls, setShowControls] = useState(false)
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false)
const subtitleBtnRef = useRef(null)
const {
reloadVideo,
togglePlay,
@ -219,12 +241,10 @@ const closeSubtitleManager = useCallback(()=> {
useVideoPlayerHotKeys(hotkeyHandlers);
const updateProgress = useCallback(() => {
console.log('currentTime2')
const player = playerRef?.current;
if (!player || typeof player?.currentTime !== 'function') return;
const currentTime = player.currentTime();
console.log('currentTime3', currentTime)
if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
setProgress(videoLocation, currentTime);
setLocalProgress(currentTime);
@ -251,7 +271,6 @@ const closeSubtitleManager = useCallback(()=> {
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
try {
const video = e.currentTarget;
console.log('onVolumeChangeHandler')
setVolume(video.volume);
setIsMuted(video.muted);
} catch (error) {
@ -271,7 +290,6 @@ const closeSubtitleManager = useCallback(()=> {
};
}, [showControls]);
console.log('isFullscreen', isFullscreen, showControlsFullScreen)
const videoStylesVideo = useMemo(() => {
return {
@ -318,11 +336,9 @@ const closeSubtitleManager = useCallback(()=> {
const enterFullscreen = () => {
const ref = containerRef?.current as any;
console.log('refffff', ref)
if (!ref) return;
if (ref.requestFullscreen && !isFullscreen) {
console.log('requset ')
ref.requestFullscreen();
}
@ -399,7 +415,102 @@ useEffect(() => {
};
}, []);
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)=> {
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);
// 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);
@ -413,7 +524,6 @@ useEffect(() => {
useEffect(() => {
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return;
console.log("EFFECT TRIGGERED", { isReady, resourceUrl, startPlay, poster, videoLocactionStringified });
const resource = JSON.parse(videoLocactionStringified)
let canceled = false;
@ -437,7 +547,6 @@ useEffect(() => {
},
],
};
console.log('options', options)
const ref = videoRef as any;
if (!ref.current) return;
@ -448,13 +557,7 @@ useEffect(() => {
playerRef.current?.playbackRate(playbackRate)
playerRef.current?.volume(volume);
playerRef.current?.addRemoteTextTrack({
kind: 'subtitles',
src: 'http://127.0.0.1:22393/arbitrary/DOCUMENT/a-test/test-identifier',
srclang: 'en',
label: 'English',
default: true
}, true);
playerRef.current?.play();
});
@ -472,7 +575,6 @@ useEffect(() => {
console.error('useEffect start player', error)
}
return () => {
console.log('canceled')
canceled = true;
const player = playerRef.current;
@ -490,12 +592,10 @@ useEffect(() => {
useEffect(() => {
if(!isPlayerInitialized) return
const player = playerRef?.current;
console.log('player rate', player)
if (!player) return;
const handleRateChange = () => {
const newRate = player?.playbackRate();
console.log('Playback rate changed:', newRate);
if(newRate){
setPlaybackRate(newRate); // or any other state/action
}
@ -544,11 +644,11 @@ useEffect(() => {
{isReady && (
<VideoControlsBar playbackRate={playbackRate} increaseSpeed={hotkeyHandlers.increaseSpeed}
<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 close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} />
<SubtitleManager subtitleBtnRef={subtitleBtnRef} close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} onSelect={onSelectSubtitle} />
</VideoContainer>
</>
);

View File

@ -62,13 +62,11 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
useEffect(() => {
if (videoLocation && isPlayerInitialized) {
console.log('hellohhhh5')
try {
const ref = playerRef as any;
if (!ref.current) return;
const savedProgress = getProgress(videoLocation);
console.log('savedProgress', savedProgress)
if (typeof savedProgress === "number") {
playerRef.current?.currentTime(savedProgress);

View File

@ -0,0 +1,4 @@
import { Service } from "../../types/interfaces/resources";
export const ENTITY_SUBTITLE = "ENTITY_SUBTITLE";
export const SERVICE_SUBTITLE: Service = "FILE"

View File

@ -0,0 +1,9 @@
import React, { useMemo } from "react";
import { useListStore } from "../state/lists";
import { useCacheStore } from "../state/cache"; // Assuming you export getResourceCache
import { QortalGetMetadata } from "../types/interfaces/resources";
export function useListReturn(listName: string): QortalGetMetadata[] {
const list = useListStore((state) => state.lists[listName]?.items) || [];
return list
}

View File

@ -28,6 +28,7 @@ export const useResources = (retryAttempts: number = 2) => {
const addTemporaryResource = useCacheStore((s) => s.addTemporaryResource);
const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted);
const setSearchParamsForList = useCacheStore((s) => s.setSearchParamsForList);
const addList = useListStore((s) => s.addList);
const deleteList = useListStore(state => state.deleteList)
const requestControllers = new Map<string, AbortController>();
@ -204,9 +205,10 @@ export const useResources = (retryAttempts: number = 2) => {
if (cancelRequests) {
cancelAllRequests();
}
console.log('listName', listName)
const cacheKey = generateCacheKey(params);
const searchCache = getSearchCache(listName, cacheKey);
console.log('searchCache', searchCache)
if (searchCache) {
const copyParams = {...params}
delete copyParams.after
@ -219,9 +221,12 @@ export const useResources = (retryAttempts: number = 2) => {
let responseData: QortalMetadata[] = [];
let filteredResults: QortalMetadata[] = [];
let lastCreated = params.before || undefined;
console.log('lastCreated', lastCreated)
const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20
const isUnlimited = params.limit === 0;
while (filteredResults.length < targetLimit) {
while (isUnlimited || filteredResults.length < targetLimit) {
console.log('beforebefore')
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
mode: "ALL",
@ -229,27 +234,31 @@ export const useResources = (retryAttempts: number = 2) => {
limit: targetLimit - filteredResults.length, // Adjust limit dynamically
before: lastCreated,
});
console.log('responseresponse', response)
if (!response || response.length === 0) {
break; // No more data available
}
responseData = response;
const validResults = responseData.filter((item) => item.size !== 32);
console.log('validResults', validResults)
filteredResults = [...filteredResults, ...validResults];
if (filteredResults.length >= targetLimit) {
if (filteredResults.length >= targetLimit && !isUnlimited) {
filteredResults = filteredResults.slice(0, targetLimit);
break;
}
lastCreated = responseData[responseData.length - 1]?.created;
if (isUnlimited) break;
if (!lastCreated) break;
}
const copyParams = {...params}
delete copyParams.after
delete copyParams.before
delete copyParams.offset
console.log('listName2', listName, filteredResults)
setSearchCache(listName, cacheKey, filteredResults, cancelRequests ? JSON.stringify(copyParams) : null);
fetchDataFromResults(filteredResults, returnType);
@ -349,7 +358,6 @@ export const useResources = (retryAttempts: number = 2) => {
return true;
}, []);
return useMemo(() => ({
fetchResources,
@ -357,8 +365,9 @@ export const useResources = (retryAttempts: number = 2) => {
updateNewResources,
deleteResource,
deleteList,
addList,
fetchResourcesResultsOnly
}), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly]);
}), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly, addList]);
};

View File

@ -6,6 +6,7 @@ export { useModal } from './hooks/useModal';
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls';
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
export { VideoPlayer } from './components/VideoPlayer/VideoPlayer';
export { useListReturn } from './hooks/useListData';
import './index.css'
export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events';
export { formatBytes, formatDuration } from './utils/numbers';

View File

@ -51,7 +51,7 @@ export type Service =
| "VOICE_PRIVATE"
| "DOCUMENT_PRIVATE"
| "MAIL_PRIVATE"
| "MESSAGE_PRIVATE";
| "MESSAGE_PRIVATE" | 'AUTO_UPDATE';
export interface QortalMetadata {

View File

@ -1,3 +1,4 @@
import { Service } from "../interfaces/resources";
import {
Coin,
ConfirmationStatus,
@ -8,7 +9,6 @@ import {
ForeignCoin,
ResourcePointer,
ResourceToPublish,
Service,
TxType,
} from "./types";

View File

@ -1,3 +1,5 @@
import { Service } from "../interfaces/resources"
export type ForeignCoin =
| 'BTC'
| 'LTC'
@ -31,61 +33,7 @@ export type ForeignCoin =
qortalAtAddress: string;
}
export type Service =
| 'AUTO_UPDATE'
| 'ARBITRARY_DATA'
| 'QCHAT_ATTACHMENT'
| 'QCHAT_ATTACHMENT_PRIVATE'
| 'ATTACHMENT'
| 'ATTACHMENT_PRIVATE'
| 'FILE'
| 'FILE_PRIVATE'
| 'FILES'
| 'CHAIN_DATA'
| 'WEBSITE'
| 'GIT_REPOSITORY'
| 'IMAGE'
| 'IMAGE_PRIVATE'
| 'THUMBNAIL'
| 'QCHAT_IMAGE'
| 'VIDEO'
| 'VIDEO_PRIVATE'
| 'AUDIO'
| 'AUDIO_PRIVATE'
| 'QCHAT_AUDIO'
| 'QCHAT_VOICE'
| 'VOICE'
| 'VOICE_PRIVATE'
| 'PODCAST'
| 'BLOG'
| 'BLOG_POST'
| 'BLOG_COMMENT'
| 'DOCUMENT'
| 'DOCUMENT_PRIVATE'
| 'LIST'
| 'PLAYLIST'
| 'APP'
| 'METADATA'
| 'JSON'
| 'GIF_REPOSITORY'
| 'STORE'
| 'PRODUCT'
| 'OFFER'
| 'COUPON'
| 'CODE'
| 'PLUGIN'
| 'EXTENSION'
| 'GAME'
| 'ITEM'
| 'NFT'
| 'DATABASE'
| 'SNAPSHOT'
| 'COMMENT'
| 'CHAIN_COMMENT'
| 'MAIL'
| 'MAIL_PRIVATE'
| 'MESSAGE'
| 'MESSAGE_PRIVATE'
export type ResourceToPublish =

View File

@ -118,4 +118,24 @@ export function base64ToObject(base64: string){
const toObject = uint8ArrayToObject(toUint);
return toObject
}
}
export const base64ToBlobUrl = (base64: string, mimeType = 'text/vtt'): string => {
console.log('base64ToBlobUrl', base64, mimeType)
const cleanedBase64 = base64.length % 4 === 0 ? base64 : base64 + '='.repeat(4 - base64.length % 4);
try {
const binary = atob(cleanedBase64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
const blob = new Blob([bytes], { type: mimeType });
return URL.createObjectURL(blob);
} catch (err) {
console.error("Failed to decode base64:", err);
return '';
}
};