mirror of
https://github.com/mue/mue.git
synced 2026-07-06 16:04:20 +02:00
fix: custom video background
This commit is contained in:
@@ -127,4 +127,4 @@ function ShareModal({ modalClose, data }) {
|
||||
|
||||
const MemoizedSharemodal = memo(ShareModal);
|
||||
|
||||
export { MemoizedSharemodal as default, MemoizedSharemodal as ShareModal };
|
||||
export { MemoizedSharemodal as default, MemoizedSharemodal as ShareModal };
|
||||
@@ -1,13 +1,19 @@
|
||||
/**
|
||||
* If the URL starts with `data:video/` or ends with `.mp4`, `.webm`, or `.ogg`, then it's a video.
|
||||
* @param url - The URL of the file to be checked.
|
||||
* @returns A function that takes a url and returns a boolean.
|
||||
* Checks if the given URL or MIME type represents a video file.
|
||||
* Supports both URLs (data:video/, .mp4, .webm, .ogg) and MIME types (video/mp4, video/webm, video/ogg).
|
||||
* @param urlOrMimeType - The URL or MIME type to check.
|
||||
* @returns true if it's a video, false otherwise.
|
||||
*/
|
||||
export default function videoCheck(url) {
|
||||
export default function videoCheck(urlOrMimeType) {
|
||||
if (!urlOrMimeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
url.startsWith('data:video/') ||
|
||||
url.endsWith('.mp4') ||
|
||||
url.endsWith('.webm') ||
|
||||
url.endsWith('.ogg')
|
||||
urlOrMimeType.startsWith('data:video/') ||
|
||||
urlOrMimeType.startsWith('video/') ||
|
||||
urlOrMimeType.endsWith('.mp4') ||
|
||||
urlOrMimeType.endsWith('.webm') ||
|
||||
urlOrMimeType.endsWith('.ogg')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
calculateStorageSize,
|
||||
calculateTotalStorageSize,
|
||||
formatBytes,
|
||||
extractVideoThumbnail,
|
||||
} from 'utils/imageMetadata';
|
||||
import { generateBlurHashDataUrl } from '../api/blurHash';
|
||||
|
||||
@@ -132,6 +133,7 @@ const CustomSettings = memo(() => {
|
||||
fileSize: metadata.fileSize,
|
||||
folder: metadata.folder || '',
|
||||
blurHash: metadata.blurHash,
|
||||
thumbnail: metadata.thumbnail || null,
|
||||
};
|
||||
|
||||
await addBackground(backgroundData);
|
||||
@@ -189,17 +191,37 @@ const CustomSettings = memo(() => {
|
||||
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onloadend = () => {
|
||||
resolve({
|
||||
dataUrl: reader.result,
|
||||
metadata: {
|
||||
name: getFileName(file, customBackground.length),
|
||||
dimensions: null,
|
||||
fileSize: file.size,
|
||||
folder: folderName,
|
||||
blurHash: null,
|
||||
},
|
||||
});
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
// Extract thumbnail and dimensions from video
|
||||
const { thumbnail, dimensions } = await extractVideoThumbnail(reader.result);
|
||||
|
||||
resolve({
|
||||
dataUrl: reader.result,
|
||||
metadata: {
|
||||
name: getFileName(file, customBackground.length),
|
||||
dimensions,
|
||||
fileSize: file.size,
|
||||
folder: folderName,
|
||||
blurHash: null,
|
||||
thumbnail,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Could not extract video thumbnail:', error);
|
||||
// Fallback to no thumbnail if extraction fails
|
||||
resolve({
|
||||
dataUrl: reader.result,
|
||||
metadata: {
|
||||
name: getFileName(file, customBackground.length),
|
||||
dimensions: null,
|
||||
fileSize: file.size,
|
||||
folder: folderName,
|
||||
blurHash: null,
|
||||
thumbnail: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
@@ -625,7 +647,7 @@ const CustomSettings = memo(() => {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="selection-separator">·</span>
|
||||
{customBackground.length > 0 && <span className="selection-separator">·</span>}
|
||||
{selectedImages.size > 0 ? (
|
||||
<>
|
||||
<span className="selected-count">{selectedImages.size} selected</span>
|
||||
@@ -757,9 +779,39 @@ const CustomSettings = memo(() => {
|
||||
) : null;
|
||||
})()}
|
||||
{isVideo ? (
|
||||
<div className="video-icon-wrapper">
|
||||
<MdPersonalVideo className="customvideoicon" />
|
||||
</div>
|
||||
bg.thumbnail ? (
|
||||
<>
|
||||
<img
|
||||
alt={bg.name || 'Custom video'}
|
||||
src={bg.thumbnail}
|
||||
loading="lazy"
|
||||
style={{ position: 'relative', zIndex: 1 }}
|
||||
/>
|
||||
<div
|
||||
className="video-overlay-icon"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '8px',
|
||||
zIndex: 2,
|
||||
background: 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<MdPersonalVideo
|
||||
style={{ fontSize: '20px', color: 'white' }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="video-icon-wrapper">
|
||||
<MdPersonalVideo className="customvideoicon" />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
alt={bg.name || 'Custom background'}
|
||||
|
||||
@@ -158,3 +158,56 @@ export function formatBytes(bytes) {
|
||||
|
||||
return Math.round((bytes / Math.pow(k, i)) * 10) / 10 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first frame of a video as a thumbnail
|
||||
* @param {string} videoDataUrl - Video data URL
|
||||
* @param {number} maxWidth - Maximum width for thumbnail (default: 320)
|
||||
* @returns {Promise<{thumbnail: string, dimensions: {width: number, height: number}}>}
|
||||
*/
|
||||
export async function extractVideoThumbnail(videoDataUrl, maxWidth = 320) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
// Seek to 0.1 seconds to get a better frame than the first black frame
|
||||
video.currentTime = 0.1;
|
||||
};
|
||||
|
||||
video.onseeked = () => {
|
||||
try {
|
||||
// Calculate thumbnail dimensions maintaining aspect ratio
|
||||
const scale = maxWidth / video.videoWidth;
|
||||
const thumbnailWidth = Math.floor(video.videoWidth * scale);
|
||||
const thumbnailHeight = Math.floor(video.videoHeight * scale);
|
||||
|
||||
// Create canvas and draw the video frame
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = thumbnailWidth;
|
||||
canvas.height = thumbnailHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, thumbnailWidth, thumbnailHeight);
|
||||
|
||||
// Convert to data URL (JPEG for better compression)
|
||||
const thumbnail = canvas.toDataURL('image/jpeg', 0.8);
|
||||
|
||||
resolve({
|
||||
thumbnail,
|
||||
dimensions: {
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to extract video thumbnail: ' + error.message));
|
||||
}
|
||||
};
|
||||
|
||||
video.onerror = () => reject(new Error('Failed to load video'));
|
||||
|
||||
video.src = videoDataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user