From be1ae20d9089e81cb84a8294be76fe680a24b60f Mon Sep 17 00:00:00 2001 From: Hyteq Date: Sun, 20 Jul 2025 11:00:22 +0300 Subject: format --- apps/web/src/lib/ffmpeg-utils.ts | 455 +++++++++++++++++++++------------------ 1 file changed, 247 insertions(+), 208 deletions(-) (limited to 'apps/web/src/lib/ffmpeg-utils.ts') diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts index ae0c267b..80d28c21 100644 --- a/apps/web/src/lib/ffmpeg-utils.ts +++ b/apps/web/src/lib/ffmpeg-utils.ts @@ -1,231 +1,270 @@ -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; 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; + 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 + 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); + 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 + 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; + 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; +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())); - - // Capture FFmpeg stderr output with a one-time listener pattern - let ffmpegOutput = ''; - let listening = true; - const listener = (data: string) => { - if (listening) ffmpegOutput += data; - }; - ffmpeg.on('log', ({ message }) => listener(message)); - - // Run ffmpeg to get info (stderr will contain the info) - try { - 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.'); - } - - // Disable listener after exec completes - listening = false; - - // Cleanup - await ffmpeg.deleteFile(inputName); - - // Parse output for duration, resolution, and fps - // Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s - // Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc - - const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/); - let duration = 0; - if (durationMatch) { - const [, h, m, s] = durationMatch; - 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; - if (videoStreamMatch) { - width = parseInt(videoStreamMatch[1]); - height = parseInt(videoStreamMatch[2]); - fps = parseFloat(videoStreamMatch[3]); - } - - return { - duration, - width, - height, - fps - }; + const ffmpeg = await initFFmpeg(); + + const inputName = "input.mp4"; + + // Write input file + await ffmpeg.writeFile( + inputName, + new Uint8Array(await videoFile.arrayBuffer()), + ); + + // Capture FFmpeg stderr output with a one-time listener pattern + let ffmpegOutput = ""; + let listening = true; + const listener = (data: string) => { + if (listening) ffmpegOutput += data; + }; + ffmpeg.on("log", ({ message }) => listener(message)); + + // Run ffmpeg to get info (stderr will contain the info) + try { + 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.", + ); + } + + // Disable listener after exec completes + listening = false; + + // Cleanup + await ffmpeg.deleteFile(inputName); + + // Parse output for duration, resolution, and fps + // Example: Duration: 00:00:10.00, start: 0.000000, bitrate: 1234 kb/s + // Example: Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 30 fps, 30 tbr, 90k tbn, 60 tbc + + const durationMatch = ffmpegOutput.match(/Duration: (\d+):(\d+):([\d.]+)/); + let duration = 0; + if (durationMatch) { + const [, h, m, s] = durationMatch; + 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; + if (videoStreamMatch) { + width = parseInt(videoStreamMatch[1]); + height = parseInt(videoStreamMatch[2]); + fps = parseFloat(videoStreamMatch[3]); + } + + return { + duration, + width, + height, + fps, + }; }; export const convertToWebM = async ( - videoFile: File, - onProgress?: (progress: number) => void + 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; + 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' + 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 + 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; +}; -- cgit v1.3.1