mirror of
https://github.com/Qortal/q-tube.git
synced 2025-12-12 15:52:59 +00:00
added mediaInfo
This commit is contained in:
BIN
public/MediaInfoModule.wasm
Normal file
BIN
public/MediaInfoModule.wasm
Normal file
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
73
src/hooks/useMediaInfo.tsx
Normal file
73
src/hooks/useMediaInfo.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user