feat: add media converter app and fix compilation errors
This commit is contained in:
114
lib/media/utils/fileUtils.ts
Normal file
114
lib/media/utils/fileUtils.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Format file size in human-readable format
|
||||
*/
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${Math.round(bytes / Math.pow(k, i) * 100) / 100} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file size (max 500MB for browser processing)
|
||||
*/
|
||||
export function validateFileSize(file: File, maxSizeMB: number = 500): boolean {
|
||||
const maxBytes = maxSizeMB * 1024 * 1024;
|
||||
return file.size <= maxBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension from filename
|
||||
*/
|
||||
export function getFileExtension(filename: string): string {
|
||||
const lastDot = filename.lastIndexOf('.');
|
||||
return lastDot === -1 ? '' : filename.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filename without extension
|
||||
*/
|
||||
export function getFilenameWithoutExtension(filename: string): string {
|
||||
const lastDot = filename.lastIndexOf('.');
|
||||
return lastDot === -1 ? filename : filename.substring(0, lastDot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate output filename
|
||||
*/
|
||||
export function generateOutputFilename(inputFilename: string, outputExtension: string): string {
|
||||
const basename = getFilenameWithoutExtension(inputFilename);
|
||||
return `${basename}.${outputExtension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download blob as file
|
||||
*/
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as ArrayBuffer
|
||||
*/
|
||||
export async function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as Data URL
|
||||
*/
|
||||
export async function readFileAsDataURL(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file type against allowed MIME types
|
||||
*/
|
||||
export function validateFileType(file: File, allowedTypes: string[]): boolean {
|
||||
return allowedTypes.some((type) => {
|
||||
if (type.endsWith('/*')) {
|
||||
const category = type.split('/')[0];
|
||||
return file.type.startsWith(`${category}/`);
|
||||
}
|
||||
return file.type === type;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download multiple blobs as a ZIP file
|
||||
*/
|
||||
export async function downloadBlobsAsZip(files: Array<{ blob: Blob; filename: string }>, zipFilename: string): Promise<void> {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
// Add all files to ZIP
|
||||
files.forEach(({ blob, filename }) => {
|
||||
zip.file(filename, blob);
|
||||
});
|
||||
|
||||
// Generate ZIP blob
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
|
||||
// Download ZIP
|
||||
downloadBlob(zipBlob, zipFilename);
|
||||
}
|
||||
272
lib/media/utils/formatMappings.ts
Normal file
272
lib/media/utils/formatMappings.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import type { ConversionFormat, FormatPreset } from '@/types/media';
|
||||
|
||||
/**
|
||||
* All supported conversion formats
|
||||
*/
|
||||
export const SUPPORTED_FORMATS: ConversionFormat[] = [
|
||||
// Video formats (FFmpeg)
|
||||
{
|
||||
id: 'mp4',
|
||||
name: 'MP4',
|
||||
extension: 'mp4',
|
||||
mimeType: 'video/mp4',
|
||||
category: 'video',
|
||||
converter: 'ffmpeg',
|
||||
description: 'MPEG-4 video format',
|
||||
},
|
||||
{
|
||||
id: 'webm',
|
||||
name: 'WebM',
|
||||
extension: 'webm',
|
||||
mimeType: 'video/webm',
|
||||
category: 'video',
|
||||
converter: 'ffmpeg',
|
||||
description: 'WebM video format',
|
||||
},
|
||||
{
|
||||
id: 'avi',
|
||||
name: 'AVI',
|
||||
extension: 'avi',
|
||||
mimeType: 'video/x-msvideo',
|
||||
category: 'video',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Audio Video Interleave',
|
||||
},
|
||||
{
|
||||
id: 'mov',
|
||||
name: 'MOV',
|
||||
extension: 'mov',
|
||||
mimeType: 'video/quicktime',
|
||||
category: 'video',
|
||||
converter: 'ffmpeg',
|
||||
description: 'QuickTime movie',
|
||||
},
|
||||
{
|
||||
id: 'mkv',
|
||||
name: 'MKV',
|
||||
extension: 'mkv',
|
||||
mimeType: 'video/x-matroska',
|
||||
category: 'video',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Matroska video',
|
||||
},
|
||||
|
||||
// Audio formats (FFmpeg)
|
||||
{
|
||||
id: 'mp3',
|
||||
name: 'MP3',
|
||||
extension: 'mp3',
|
||||
mimeType: 'audio/mpeg',
|
||||
category: 'audio',
|
||||
converter: 'ffmpeg',
|
||||
description: 'MPEG audio layer 3',
|
||||
},
|
||||
{
|
||||
id: 'wav',
|
||||
name: 'WAV',
|
||||
extension: 'wav',
|
||||
mimeType: 'audio/wav',
|
||||
category: 'audio',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Waveform audio',
|
||||
},
|
||||
{
|
||||
id: 'ogg',
|
||||
name: 'OGG',
|
||||
extension: 'ogg',
|
||||
mimeType: 'audio/ogg',
|
||||
category: 'audio',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Ogg Vorbis audio',
|
||||
},
|
||||
{
|
||||
id: 'aac',
|
||||
name: 'AAC',
|
||||
extension: 'aac',
|
||||
mimeType: 'audio/aac',
|
||||
category: 'audio',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Advanced Audio Coding',
|
||||
},
|
||||
{
|
||||
id: 'flac',
|
||||
name: 'FLAC',
|
||||
extension: 'flac',
|
||||
mimeType: 'audio/flac',
|
||||
category: 'audio',
|
||||
converter: 'ffmpeg',
|
||||
description: 'Free Lossless Audio Codec',
|
||||
},
|
||||
|
||||
// Image formats (ImageMagick)
|
||||
{
|
||||
id: 'png',
|
||||
name: 'PNG',
|
||||
extension: 'png',
|
||||
mimeType: 'image/png',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'Portable Network Graphics',
|
||||
},
|
||||
{
|
||||
id: 'jpg',
|
||||
name: 'JPG',
|
||||
extension: 'jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'JPEG image',
|
||||
},
|
||||
{
|
||||
id: 'webp',
|
||||
name: 'WebP',
|
||||
extension: 'webp',
|
||||
mimeType: 'image/webp',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'WebP image format',
|
||||
},
|
||||
{
|
||||
id: 'gif',
|
||||
name: 'GIF',
|
||||
extension: 'gif',
|
||||
mimeType: 'image/gif',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'Graphics Interchange Format',
|
||||
},
|
||||
{
|
||||
id: 'bmp',
|
||||
name: 'BMP',
|
||||
extension: 'bmp',
|
||||
mimeType: 'image/bmp',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'Bitmap image',
|
||||
},
|
||||
{
|
||||
id: 'tiff',
|
||||
name: 'TIFF',
|
||||
extension: 'tiff',
|
||||
mimeType: 'image/tiff',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'Tagged Image File Format',
|
||||
},
|
||||
{
|
||||
id: 'svg',
|
||||
name: 'SVG',
|
||||
extension: 'svg',
|
||||
mimeType: 'image/svg+xml',
|
||||
category: 'image',
|
||||
converter: 'imagemagick',
|
||||
description: 'Scalable Vector Graphics',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Format presets for common conversions
|
||||
*/
|
||||
export const FORMAT_PRESETS: FormatPreset[] = [
|
||||
{
|
||||
id: 'web-video',
|
||||
name: 'Web Video',
|
||||
description: 'Optimize video for web playback (VP8 for better compatibility)',
|
||||
category: 'video',
|
||||
sourceFormats: ['mp4', 'avi', 'mov', 'mkv'],
|
||||
targetFormat: 'webm',
|
||||
options: {
|
||||
videoCodec: 'libvpx',
|
||||
videoBitrate: '1M',
|
||||
audioCodec: 'libvorbis',
|
||||
audioBitrate: '128k',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'web-image',
|
||||
name: 'Web Image',
|
||||
description: 'Optimize image for web',
|
||||
category: 'image',
|
||||
sourceFormats: ['png', 'jpg', 'bmp', 'tiff'],
|
||||
targetFormat: 'webp',
|
||||
options: {
|
||||
imageQuality: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'audio-compress',
|
||||
name: 'Compress Audio',
|
||||
description: 'Reduce audio file size',
|
||||
category: 'audio',
|
||||
sourceFormats: ['wav', 'flac'],
|
||||
targetFormat: 'mp3',
|
||||
options: {
|
||||
audioBitrate: '192k',
|
||||
audioCodec: 'libmp3lame',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'video-gif',
|
||||
name: 'Video to GIF',
|
||||
description: 'Convert video to animated GIF',
|
||||
category: 'video',
|
||||
sourceFormats: ['mp4', 'webm', 'avi', 'mov'],
|
||||
targetFormat: 'gif',
|
||||
options: {
|
||||
videoFps: 15,
|
||||
videoResolution: '480x-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get format by ID
|
||||
*/
|
||||
export function getFormatById(id: string): ConversionFormat | undefined {
|
||||
return SUPPORTED_FORMATS.find((f) => f.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get format by extension
|
||||
*/
|
||||
export function getFormatByExtension(extension: string): ConversionFormat | undefined {
|
||||
return SUPPORTED_FORMATS.find((f) => f.extension === extension.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get format by MIME type
|
||||
*/
|
||||
export function getFormatByMimeType(mimeType: string): ConversionFormat | undefined {
|
||||
return SUPPORTED_FORMATS.find((f) => f.mimeType === mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all formats by category
|
||||
*/
|
||||
export function getFormatsByCategory(category: string): ConversionFormat[] {
|
||||
return SUPPORTED_FORMATS.filter((f) => f.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compatible output formats for input format
|
||||
*/
|
||||
export function getCompatibleFormats(inputFormat: ConversionFormat): ConversionFormat[] {
|
||||
// Same category and same converter
|
||||
return SUPPORTED_FORMATS.filter(
|
||||
(f) => f.category === inputFormat.category && f.converter === inputFormat.converter && f.id !== inputFormat.id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if conversion is supported
|
||||
*/
|
||||
export function isConversionSupported(
|
||||
inputFormat: ConversionFormat,
|
||||
outputFormat: ConversionFormat
|
||||
): boolean {
|
||||
return (
|
||||
inputFormat.category === outputFormat.category &&
|
||||
inputFormat.converter === outputFormat.converter &&
|
||||
inputFormat.id !== outputFormat.id
|
||||
);
|
||||
}
|
||||
188
lib/media/utils/formatPresets.ts
Normal file
188
lib/media/utils/formatPresets.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { ConversionOptions } from '@/types/media';
|
||||
|
||||
export interface FormatPreset {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
category: 'video' | 'audio' | 'image';
|
||||
outputFormat: string;
|
||||
options: ConversionOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined format presets for common use cases
|
||||
*/
|
||||
export const FORMAT_PRESETS: FormatPreset[] = [
|
||||
// Video Presets
|
||||
{
|
||||
id: 'youtube-video',
|
||||
name: 'YouTube Video',
|
||||
description: '1080p MP4, optimized for YouTube',
|
||||
icon: '🎬',
|
||||
category: 'video',
|
||||
outputFormat: 'mp4',
|
||||
options: {
|
||||
videoCodec: 'libx264',
|
||||
videoBitrate: '5M',
|
||||
videoResolution: '1920x-1',
|
||||
videoFps: 30,
|
||||
audioCodec: 'aac',
|
||||
audioBitrate: '192k',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'instagram-video',
|
||||
name: 'Instagram Video',
|
||||
description: 'Square 1:1 format for Instagram',
|
||||
icon: '📸',
|
||||
category: 'video',
|
||||
outputFormat: 'mp4',
|
||||
options: {
|
||||
videoCodec: 'libx264',
|
||||
videoBitrate: '3M',
|
||||
videoResolution: '1080x-1', // Will be cropped to square
|
||||
videoFps: 30,
|
||||
audioCodec: 'aac',
|
||||
audioBitrate: '128k',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'twitter-video',
|
||||
name: 'Twitter Video',
|
||||
description: '720p, optimized for Twitter',
|
||||
icon: '🐦',
|
||||
category: 'video',
|
||||
outputFormat: 'mp4',
|
||||
options: {
|
||||
videoCodec: 'libx264',
|
||||
videoBitrate: '2M',
|
||||
videoResolution: '1280x-1',
|
||||
videoFps: 30,
|
||||
audioCodec: 'aac',
|
||||
audioBitrate: '128k',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'web-video',
|
||||
name: 'Web Optimized',
|
||||
description: 'Small file size for web streaming',
|
||||
icon: '🌐',
|
||||
category: 'video',
|
||||
outputFormat: 'mp4',
|
||||
options: {
|
||||
videoCodec: 'libx264',
|
||||
videoBitrate: '1.5M',
|
||||
videoResolution: '854x-1',
|
||||
videoFps: 24,
|
||||
audioCodec: 'aac',
|
||||
audioBitrate: '96k',
|
||||
},
|
||||
},
|
||||
|
||||
// Audio Presets
|
||||
{
|
||||
id: 'podcast-audio',
|
||||
name: 'Podcast',
|
||||
description: 'MP3, optimized for voice',
|
||||
icon: '🎙️',
|
||||
category: 'audio',
|
||||
outputFormat: 'mp3',
|
||||
options: {
|
||||
audioCodec: 'libmp3lame',
|
||||
audioBitrate: '128k',
|
||||
audioSampleRate: 44100,
|
||||
audioChannels: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'music-high-quality',
|
||||
name: 'High Quality Music',
|
||||
description: 'MP3, 320kbps for music',
|
||||
icon: '🎵',
|
||||
category: 'audio',
|
||||
outputFormat: 'mp3',
|
||||
options: {
|
||||
audioCodec: 'libmp3lame',
|
||||
audioBitrate: '320k',
|
||||
audioSampleRate: 48000,
|
||||
audioChannels: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'audiobook',
|
||||
name: 'Audiobook',
|
||||
description: 'Mono, small file size',
|
||||
icon: '📚',
|
||||
category: 'audio',
|
||||
outputFormat: 'mp3',
|
||||
options: {
|
||||
audioCodec: 'libmp3lame',
|
||||
audioBitrate: '64k',
|
||||
audioSampleRate: 22050,
|
||||
audioChannels: 1,
|
||||
},
|
||||
},
|
||||
|
||||
// Image Presets
|
||||
{
|
||||
id: 'web-thumbnail',
|
||||
name: 'Web Thumbnail',
|
||||
description: 'JPG, 800px width, optimized',
|
||||
icon: '🖼️',
|
||||
category: 'image',
|
||||
outputFormat: 'jpg',
|
||||
options: {
|
||||
imageQuality: 85,
|
||||
imageWidth: 800,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hd-image',
|
||||
name: 'HD Image',
|
||||
description: 'PNG, high quality, lossless',
|
||||
icon: '🎨',
|
||||
category: 'image',
|
||||
outputFormat: 'png',
|
||||
options: {
|
||||
imageQuality: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'social-media-image',
|
||||
name: 'Social Media',
|
||||
description: 'JPG, 1200px, optimized',
|
||||
icon: '📱',
|
||||
category: 'image',
|
||||
outputFormat: 'jpg',
|
||||
options: {
|
||||
imageQuality: 90,
|
||||
imageWidth: 1200,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'web-optimized-image',
|
||||
name: 'Web Optimized',
|
||||
description: 'WebP, small file size',
|
||||
icon: '⚡',
|
||||
category: 'image',
|
||||
outputFormat: 'webp',
|
||||
options: {
|
||||
imageQuality: 80,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get presets by category
|
||||
*/
|
||||
export function getPresetsByCategory(category: 'video' | 'audio' | 'image'): FormatPreset[] {
|
||||
return FORMAT_PRESETS.filter(preset => preset.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preset by ID
|
||||
*/
|
||||
export function getPresetById(id: string): FormatPreset | undefined {
|
||||
return FORMAT_PRESETS.find(preset => preset.id === id);
|
||||
}
|
||||
Reference in New Issue
Block a user