diff options
| author | Kha Nguyen <hi@khanguyen.com> | 2025-07-24 17:00:49 -0500 |
|---|---|---|
| committer | Kha Nguyen <hi@khanguyen.com> | 2025-07-24 17:00:49 -0500 |
| commit | 1ccacf6bb5db579cf6f343d9de57bce92d296f5f (patch) | |
| tree | 8f4ab02ef1c0a7420c1827607207c52629e56288 /apps/web/src/lib/ffmpeg-utils.ts | |
| parent | 0b7a943de30663a37aec93421cbd90dfbd673d87 (diff) | |
| download | opencut-1ccacf6bb5db579cf6f343d9de57bce92d296f5f.tar.gz | |
merge main
Diffstat (limited to 'apps/web/src/lib/ffmpeg-utils.ts')
| -rw-r--r-- | apps/web/src/lib/ffmpeg-utils.ts | 209 |
1 files changed, 124 insertions, 85 deletions
diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts index ae0c267b..c34200f6 100644 --- a/apps/web/src/lib/ffmpeg-utils.ts +++ b/apps/web/src/lib/ffmpeg-utils.ts @@ -1,5 +1,5 @@ -import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { toBlobURL } from '@ffmpeg/util'; +import { FFmpeg } from "@ffmpeg/ffmpeg"; +import { toBlobURL } from "@ffmpeg/util"; let ffmpeg: FFmpeg | null = null; @@ -7,13 +7,13 @@ export const initFFmpeg = async (): Promise<FFmpeg> => { if (ffmpeg) return ffmpeg; ffmpeg = new FFmpeg(); - + // Use locally hosted files instead of CDN - const baseURL = '/ffmpeg'; - + const baseURL = "/ffmpeg"; + await ffmpeg.load({ - coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), + coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), + wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"), }); return ffmpeg; @@ -21,34 +21,42 @@ export const initFFmpeg = async (): Promise<FFmpeg> => { export const generateThumbnail = async ( videoFile: File, - timeInSeconds: number = 1 + timeInSeconds = 1 ): Promise<string> => { const ffmpeg = await initFFmpeg(); - - const inputName = 'input.mp4'; - const outputName = 'thumbnail.jpg'; - + + const inputName = "input.mp4"; + const outputName = "thumbnail.jpg"; + // Write input file - await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); - + 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 + "-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' }); - + const blob = new Blob([data], { type: "image/jpeg" }); + // Cleanup await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); - + return URL.createObjectURL(blob); }; @@ -59,43 +67,52 @@ export const trimVideo = async ( onProgress?: (progress: number) => void ): Promise<Blob> => { const ffmpeg = await initFFmpeg(); - - const inputName = 'input.mp4'; - const outputName = 'output.mp4'; - + + const inputName = "input.mp4"; + const outputName = "output.mp4"; + // Set up progress callback if (onProgress) { - ffmpeg.on('progress', ({ progress }) => { + ffmpeg.on("progress", ({ progress }) => { onProgress(progress * 100); }); } - + // Write input file - await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); - + 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 + "-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' }); - + 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<{ +export const getVideoInfo = async ( + videoFile: File +): Promise<{ duration: number; width: number; height: number; @@ -103,27 +120,32 @@ export const getVideoInfo = async (videoFile: File): Promise<{ }> => { const ffmpeg = await initFFmpeg(); - const inputName = 'input.mp4'; + const inputName = "input.mp4"; // Write input file - await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + await ffmpeg.writeFile( + inputName, + new Uint8Array(await videoFile.arrayBuffer()) + ); // Capture FFmpeg stderr output with a one-time listener pattern - let ffmpegOutput = ''; + let ffmpegOutput = ""; let listening = true; const listener = (data: string) => { if (listening) ffmpegOutput += data; }; - ffmpeg.on('log', ({ message }) => listener(message)); + ffmpeg.on("log", ({ message }) => listener(message)); // Run ffmpeg to get info (stderr will contain the info) try { - await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); + await ffmpeg.exec(["-i", inputName, "-f", "null", "-"]); } catch (error) { listening = false; await ffmpeg.deleteFile(inputName); - console.error('FFmpeg execution failed:', error); - throw new Error('Failed to extract video info. The file may be corrupted or in an unsupported format.'); + console.error("FFmpeg execution failed:", error); + throw new Error( + "Failed to extract video info. The file may be corrupted or in an unsupported format." + ); } // Disable listener after exec completes @@ -143,8 +165,12 @@ export const getVideoInfo = async (videoFile: File): Promise<{ duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); } - const videoStreamMatch = ffmpegOutput.match(/Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/); - let width = 0, height = 0, fps = 0; + const videoStreamMatch = ffmpegOutput.match( + /Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/ + ); + let width = 0, + height = 0, + fps = 0; if (videoStreamMatch) { width = parseInt(videoStreamMatch[1]); height = parseInt(videoStreamMatch[2]); @@ -155,7 +181,7 @@ export const getVideoInfo = async (videoFile: File): Promise<{ duration, width, height, - fps + fps, }; }; @@ -164,68 +190,81 @@ export const convertToWebM = async ( onProgress?: (progress: number) => void ): Promise<Blob> => { const ffmpeg = await initFFmpeg(); - - const inputName = 'input.mp4'; - const outputName = 'output.webm'; - + + const inputName = "input.mp4"; + const outputName = "output.webm"; + // Set up progress callback if (onProgress) { - ffmpeg.on('progress', ({ progress }) => { + ffmpeg.on("progress", ({ progress }) => { onProgress(progress * 100); }); } - + // Write input file - await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); - + 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 + "-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' }); - + 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' + format: "mp3" | "wav" = "mp3" ): Promise<Blob> => { const ffmpeg = await initFFmpeg(); - - const inputName = 'input.mp4'; + + const inputName = "input.mp4"; const outputName = `output.${format}`; - + // Write input file - await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); - + 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 + "-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 +}; |
