summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/ffmpeg-utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/ffmpeg-utils.ts')
-rw-r--r--apps/web/src/lib/ffmpeg-utils.ts431
1 files changed, 196 insertions, 235 deletions
diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts
index 80d28c21..ae0c267b 100644
--- a/apps/web/src/lib/ffmpeg-utils.ts
+++ b/apps/web/src/lib/ffmpeg-utils.ts
@@ -1,270 +1,231 @@
-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<FFmpeg> => {
- if (ffmpeg) return ffmpeg;
+ if (ffmpeg) return ffmpeg;
- ffmpeg = new 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'),
+ });
- // 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;
+ return ffmpeg;
};
export const generateThumbnail = async (
- videoFile: File,
- timeInSeconds: number = 1,
+ videoFile: File,
+ timeInSeconds: number = 1
): Promise<string> => {
- 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<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;
+ 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 ffmpeg = await initFFmpeg();
- const inputName = "input.mp4";
+ const inputName = 'input.mp4';
- // Write input file
- await ffmpeg.writeFile(
- inputName,
- new Uint8Array(await videoFile.arrayBuffer()),
- );
+ // 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));
+ // 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.",
- );
- }
+ // 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;
+ // Disable listener after exec completes
+ listening = false;
- // Cleanup
- await ffmpeg.deleteFile(inputName);
+ // 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
+ // 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 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]);
- }
+ 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,
- };
+ return {
+ duration,
+ width,
+ height,
+ fps
+ };
};
export const convertToWebM = async (
- videoFile: File,
- onProgress?: (progress: number) => void,
+ videoFile: File,
+ onProgress?: (progress: number) => void
): Promise<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;
+ 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<Blob> => {
- 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;
-};
+ 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