From 56480772c3ce5b9a2da8fb57b66cb184a52cfc97 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 10:33:03 +0300 Subject: feat: initial DnD and ffmpeg install --- apps/web/src/lib/ffmpeg-utils.ts | 197 +++++++++++++++++++++++++++++++++++ apps/web/src/lib/media-processing.ts | 7 +- 2 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/ffmpeg-utils.ts (limited to 'apps/web/src') diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts new file mode 100644 index 00000000..56085a21 --- /dev/null +++ b/apps/web/src/lib/ffmpeg-utils.ts @@ -0,0 +1,197 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { toBlobURL } from '@ffmpeg/util'; + +let ffmpeg: FFmpeg | null = null; + +export const initFFmpeg = async (): Promise => { + if (ffmpeg) return ffmpeg; + + ffmpeg = new FFmpeg(); + + // Use locally hosted files instead of CDN + const baseURL = '/ffmpeg'; + + await ffmpeg.load({ + coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), + wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), + }); + + return ffmpeg; +}; + +export const generateThumbnail = async ( + videoFile: File, + timeInSeconds: number = 1 +): Promise => { + const ffmpeg = await initFFmpeg(); + + const inputName = 'input.mp4'; + const outputName = 'thumbnail.jpg'; + + // Write input file + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + + // Generate thumbnail at specific time + await ffmpeg.exec([ + '-i', inputName, + '-ss', timeInSeconds.toString(), + '-vframes', '1', + '-vf', 'scale=320:240', + '-q:v', '2', + outputName + ]); + + // Read output file + const data = await ffmpeg.readFile(outputName); + const blob = new Blob([data], { type: 'image/jpeg' }); + + // Cleanup + await ffmpeg.deleteFile(inputName); + await ffmpeg.deleteFile(outputName); + + return URL.createObjectURL(blob); +}; + +export const trimVideo = async ( + videoFile: File, + startTime: number, + endTime: number, + onProgress?: (progress: number) => void +): Promise => { + const ffmpeg = await initFFmpeg(); + + const inputName = 'input.mp4'; + const outputName = 'output.mp4'; + + // Set up progress callback + if (onProgress) { + ffmpeg.on('progress', ({ progress }) => { + onProgress(progress * 100); + }); + } + + // Write input file + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + + const duration = endTime - startTime; + + // Trim video + await ffmpeg.exec([ + '-i', inputName, + '-ss', startTime.toString(), + '-t', duration.toString(), + '-c', 'copy', // Use stream copy for faster processing + outputName + ]); + + // Read output file + const data = await ffmpeg.readFile(outputName); + const blob = new Blob([data], { type: 'video/mp4' }); + + // Cleanup + await ffmpeg.deleteFile(inputName); + await ffmpeg.deleteFile(outputName); + + return blob; +}; + +export const getVideoInfo = async (videoFile: File): Promise<{ + duration: number; + width: number; + height: number; + fps: number; +}> => { + const ffmpeg = await initFFmpeg(); + + const inputName = 'input.mp4'; + + // Write input file + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + + // Get video info + await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); + + // Note: In a real implementation, you'd parse the FFmpeg output + // For now, we'll return default values and enhance this later + + // Cleanup + await ffmpeg.deleteFile(inputName); + + return { + duration: 10, // Placeholder - would parse from FFmpeg output + width: 1920, // Placeholder + height: 1080, // Placeholder + fps: 30 // Placeholder + }; +}; + +export const convertToWebM = async ( + videoFile: File, + onProgress?: (progress: number) => void +): Promise => { + const ffmpeg = await initFFmpeg(); + + const inputName = 'input.mp4'; + const outputName = 'output.webm'; + + // Set up progress callback + if (onProgress) { + ffmpeg.on('progress', ({ progress }) => { + onProgress(progress * 100); + }); + } + + // Write input file + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + + // Convert to WebM + await ffmpeg.exec([ + '-i', inputName, + '-c:v', 'libvpx-vp9', + '-crf', '30', + '-b:v', '0', + '-c:a', 'libopus', + outputName + ]); + + // Read output file + const data = await ffmpeg.readFile(outputName); + const blob = new Blob([data], { type: 'video/webm' }); + + // Cleanup + await ffmpeg.deleteFile(inputName); + await ffmpeg.deleteFile(outputName); + + return blob; +}; + +export const extractAudio = async ( + videoFile: File, + format: 'mp3' | 'wav' = 'mp3' +): Promise => { + const ffmpeg = await initFFmpeg(); + + const inputName = 'input.mp4'; + const outputName = `output.${format}`; + + // Write input file + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + + // Extract audio + await ffmpeg.exec([ + '-i', inputName, + '-vn', // Disable video + '-acodec', format === 'mp3' ? 'libmp3lame' : 'pcm_s16le', + outputName + ]); + + // Read output file + const data = await ffmpeg.readFile(outputName); + const blob = new Blob([data], { type: `audio/${format}` }); + + // Cleanup + await ffmpeg.deleteFile(inputName); + await ffmpeg.deleteFile(outputName); + + return blob; +}; \ No newline at end of file diff --git a/apps/web/src/lib/media-processing.ts b/apps/web/src/lib/media-processing.ts index c51dde55..d241f411 100644 --- a/apps/web/src/lib/media-processing.ts +++ b/apps/web/src/lib/media-processing.ts @@ -6,6 +6,7 @@ import { getImageAspectRatio, type MediaItem, } from "@/stores/media-store"; +// import { generateThumbnail, getVideoInfo } from "./ffmpeg-utils"; // Temporarily disabled export interface ProcessedMediaItem extends Omit {} @@ -33,7 +34,7 @@ export async function processMediaFiles( // Get image aspect ratio aspectRatio = await getImageAspectRatio(file); } else if (fileType === "video") { - // Generate thumbnail and get aspect ratio for videos + // Use basic thumbnail generation for now const videoResult = await generateVideoThumbnail(file); thumbnailUrl = videoResult.thumbnailUrl; aspectRatio = videoResult.aspectRatio; @@ -42,8 +43,8 @@ export async function processMediaFiles( aspectRatio = 1; } - // Get duration for videos and audio - if (fileType === "video" || fileType === "audio") { + // Get duration for videos and audio (if not already set by FFmpeg) + if ((fileType === "video" || fileType === "audio") && !duration) { duration = await getMediaDuration(file); } -- cgit v1.3.1