added mediaInfo

This commit is contained in:
2025-07-24 07:07:16 +03:00
parent d8acf79ad2
commit 92a0bd949b
10 changed files with 158 additions and 42 deletions

BIN
public/MediaInfoModule.wasm Normal file

Binary file not shown.

View File

@@ -71,6 +71,7 @@ import {
setNotificationAtom,
} from '../../../state/global/notifications.ts';
import { useTranslation } from 'react-i18next';
import { useMediaInfo } from '../../../hooks/useMediaInfo.tsx';
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {
@@ -146,6 +147,7 @@ export const PublishVideo = ({
useState(false);
const [imageExtracts, setImageExtracts] = useState<any>({});
const publishFromLibrary = usePublish();
const { isHEVC } = useMediaInfo();
const assembleVideoDurations = () => {
if (files.length === videoDurations.length) return;
@@ -162,33 +164,40 @@ export const PublishVideo = ({
'video/*': [],
},
maxSize,
onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map((item) => {
onDrop: async (acceptedFiles, rejectedFiles) => {
const formattedFiles = [];
for (const file of acceptedFiles) {
let filteredTitle = '';
if (isCheckTitleByFile) {
const fileName = getFileName(item?.name || '');
const fileName = getFileName(file?.name || '');
filteredTitle = (titlesPrefix + fileName).replace(titleFormatter, '');
}
return {
file: item,
const notSupportedCodec = await isHEVC(file);
console.log('isGood', isGood);
formattedFiles.push({
isHEVC: notSupportedCodec,
file,
title: filteredTitle || '',
description: '',
coverImage: '',
};
});
});
}
setFiles((prev) => [...prev, ...formatArray]);
setFiles((prev) => [...prev, ...formattedFiles]);
let errorString: string | null = null;
rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => {
for (const { file, errors } of rejectedFiles) {
for (const error of errors) {
if (error.code === 'file-too-large') {
errorString = `File must be under ${videoMaxSize}MB`;
}
console.error(`Error with file ${file.name}: ${error.message}`);
});
});
}
}
if (errorString) {
const notificationObj: AltertObject = {
msg: errorString,

View File

@@ -38,7 +38,7 @@ export const VideoPlayer = ({ ...props }: VideoPlayerProps) => {
const location = useLocation();
const { lists } = useGlobal();
const [watchedHistory, setWatchedHistory, isHydratedWatchedHistory] =
usePersistedState('watched-v1', []);
usePersistedState<any[]>('watched-v1', []);
const onPlay = useCallback(() => {
if (!isHydratedWatchedHistory) return;
const videoReference = {
@@ -79,15 +79,22 @@ export const VideoPlayer = ({ ...props }: VideoPlayerProps) => {
poster={props.poster}
videoRef={videoRef}
qortalVideoResource={{
name: props.name,
name: props.name!,
service: props.service as Service,
identifier: props.identifier,
identifier: props.identifier!,
}}
autoPlay={props?.autoPlay}
onEnded={props?.onEnd}
onPlay={onPlay}
filename={props?.filename}
path={location.pathname}
styling={{
progressSlider: {
thumbColor: 'white',
railColor: '',
trackColor: '#4285f4',
},
}}
/>
</Box>
);

View File

@@ -69,7 +69,7 @@ export const Search = () => {
'filterMode',
'recent'
);
const [searchHistory, setSearchHistory] = usePersistedState(
const [searchHistory, setSearchHistory] = usePersistedState<any[]>(
'search-history-v1',
[]
);
@@ -114,7 +114,7 @@ export const Search = () => {
}
};
const handleClickAway = (event: MouseEvent) => {
const handleClickAway = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
if (!searchWrapperRef.current?.contains(target)) {
setShowPopover(false);

View File

@@ -199,7 +199,7 @@ export const Sidenav = ({ allNames }) => {
}
}}
>
<>
<Box>
<Drawer
elevation={1}
variant="permanent"
@@ -347,7 +347,7 @@ export const Sidenav = ({ allNames }) => {
})}
</List>
</Drawer>
</>
</Box>
</ClickAwayListener>
</>
);

View File

@@ -0,0 +1,73 @@
// hooks/useMediaInfo.ts
import { useRef, useEffect } from 'react';
import mediaInfoFactory, {
type MediaInfo,
type ReadChunkFunc,
} from 'mediainfo.js';
function makeReadChunk(file: File): ReadChunkFunc {
return async (chunkSize: number, offset: number) =>
new Uint8Array(await file.slice(offset, offset + chunkSize).arrayBuffer());
}
export function useMediaInfo() {
const mediaInfoRef = useRef<MediaInfo<'text'> | null>(null);
const isReady = useRef(false);
useEffect(() => {
mediaInfoFactory({
format: 'text',
locateFile: () => '/MediaInfoModule.wasm',
})
.then((mi) => {
mediaInfoRef.current = mi;
isReady.current = true;
})
.catch((err) => {
console.error('Failed to load MediaInfo WASM:', err);
isReady.current = false;
});
return () => {
mediaInfoRef.current?.close();
};
}, []);
/**
* Analyzes the file and returns the full MediaInfo text output.
*/
async function getMediaInfo(file: File): Promise<string | null> {
if (!isReady.current || !mediaInfoRef.current) {
console.warn('MediaInfo not ready');
return null;
}
try {
return await mediaInfoRef.current.analyzeData(
file.size,
makeReadChunk(file)
);
} catch (err) {
console.error('Failed to analyze media info:', err);
return null;
}
}
/**
* Detects if the file uses HEVC (H.265) codec.
*/
async function isHEVC(file: File): Promise<boolean> {
const result = await getMediaInfo(file);
if (!result) return false;
// Normalize result and match HEVC in a more robust way
const normalized = result.toLowerCase();
return normalized.includes('format') && normalized.includes('hevc');
}
return {
getMediaInfo,
isHEVC,
isReady: isReady.current,
};
}

View File

@@ -152,8 +152,6 @@ export const Bookmarks = () => {
setIsOpenEdit(false);
};
console.log('sortedLists', sortedLists);
if (!selectedList && isHydratedBookmarks) {
return (
<PageTransition>
@@ -291,10 +289,18 @@ export const Bookmarks = () => {
})}
: {selectedList?.title}
</PageSubTitle>
<ButtonBase>
<EditIcon onClick={() => setIsOpenEdit(true)} />
</ButtonBase>
<Spacer height="14px" />
<Spacer height="10px" />
<Button
size="small"
onClick={() => setIsOpenEdit(true)}
variant="outlined"
startIcon={<EditIcon />}
>
Edit
</Button>
<Spacer height="10px" />
<Divider flexItem />
<Spacer height="20px" />
</Box>

View File

@@ -132,21 +132,19 @@ export const VideoActionsBar = ({
/>
</Box>
{videoData && (
<ButtonBase>
<FileElement
fileInfo={{
...videoReference,
filename: saveAsFilename,
mimeType: videoData?.videoType || '"video/mp4',
}}
title={videoData?.filename || videoData?.title?.slice(0, 20)}
customStyles={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
/>
</ButtonBase>
<FileElement
fileInfo={{
...videoReference,
filename: saveAsFilename,
mimeType: videoData?.videoType || '"video/mp4',
}}
title={videoData?.filename || videoData?.title?.slice(0, 20)}
customStyles={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}
/>
)}
</Box>
);

View File

@@ -8,12 +8,14 @@ import {
DialogTitle,
Divider,
FormControl,
IconButton,
InputLabel,
MenuItem,
Select,
TextField,
} from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import ClearAllIcon from '@mui/icons-material/ClearAll';
import {
QTUBE_PLAYLIST_BASE,
@@ -34,6 +36,7 @@ import { PageTransition } from '../../components/common/PageTransition.tsx';
import { useIsSmall } from '../../hooks/useIsSmall.tsx';
export const History = () => {
const { lists } = useGlobal();
const isSmall = useIsSmall();
const [watchedHistory, setWatchedHistory, isHydratedWatchedHistory] =
usePersistedState('watched-v1', []);
@@ -55,7 +58,24 @@ export const History = () => {
>
<PageSubTitle>Your History</PageSubTitle>
<Spacer height="14px" />
<Spacer height="10px" />
{watchedHistory?.length > 0 && isHydratedWatchedHistory && (
<Button
size="small"
onClick={() => {
if (isHydratedWatchedHistory) {
setWatchedHistory([]);
lists.deleteList('watched-history');
}
}}
variant="outlined"
startIcon={<ClearAllIcon />}
>
Clear
</Button>
)}
<Spacer height="10px" />
<Divider flexItem />
<Spacer height="20px" />
</Box>

View File

@@ -63,6 +63,7 @@ export const FilterOptions = () => {
const isSmall = useIsSmall();
const tabsRef = useRef(null);
const [tabIndex, setTabIndex] = useState(0);
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
@@ -161,13 +162,15 @@ export const FilterOptions = () => {
<>
{isSmall && (
<Tabs
value={tabIndex}
onChange={(event, newValue) => setTabIndex(newValue)}
ref={tabsRef}
aria-label="basic tabs example"
variant="scrollable" // Make tabs scrollable
scrollButtons={true}
sx={{
'& .MuiTabs-indicator': {
backgroundColor: 'white',
display: 'none',
},
width: `100%`, // Ensure the tabs container fits within the available space
overflow: 'hidden', // Prevents overflow on small screens