|
1 | 1 | ---
|
2 | 2 | title: "Compress a video using FFmpeg"
|
3 |
| -sidebarTitle: "Compress a video with FFmpeg" |
4 |
| -description: "This example shows you how to compress a video using FFmpeg with Trigger.dev." |
| 3 | +sidebarTitle: "FFmpeg compress video" |
| 4 | +description: "This example shows you how to compress a video using FFmpeg with Trigger.dev and upload the compressed video to R2 storage." |
5 | 5 | ---
|
6 | 6 |
|
7 | 7 | ## Overview
|
8 | 8 |
|
9 |
| -This task uses FFmpeg to compress a video, reducing its file size while attempting to maintain reasonable quality. |
| 9 | +This task demonstrates how to use FFmpeg to compress a video, reducing its file size while maintaining reasonable quality, and upload the compressed video to R2 storage using Trigger.dev. |
10 | 10 |
|
11 | 11 | ## Key Features:
|
12 | 12 |
|
13 | 13 | - Fetches a video from a given URL
|
14 |
| -- Uses the H.264 codec for video compression |
15 |
| -- Sets the Constant Rate Factor (CRF) to 28 for a good balance between quality and file size |
16 |
| -- Uses the "veryslow" preset for the best compression |
17 |
| -- Scales the video resolution to 50% of the original width and height |
18 |
| -- Uses AAC codec for audio with a bitrate of 64kbps and converts to mono |
19 |
| -- Handles temporary file management by creating and cleaning up input and output files |
20 |
| -- Returns the compressed video buffer, the file path of the compressed video, the original file size, the compressed file size, and the compression ratio |
| 14 | +- Compresses the video using FFmpeg with various compression settings |
| 15 | +- Uploads the compressed video to R2 storage |
| 16 | +- Handles temporary file management by creating and cleaning up the output file |
| 17 | +- Returns the compressed video file path, the compressed file size, and the R2 URL of the uploaded video |
| 18 | + |
| 19 | +### Extension configuration |
| 20 | + |
| 21 | +To use our FFmpeg extension during the build process, you need to add it to your project configuration like this: |
| 22 | + |
| 23 | +```ts trigger.config.ts |
| 24 | +import { ffmpeg } from "@trigger.dev/build/extensions/core"; |
| 25 | +import { defineConfig } from "@trigger.dev/sdk/v3"; |
| 26 | + |
| 27 | +export default defineConfig({ |
| 28 | + project: "<your-project-id>", |
| 29 | + |
| 30 | +... |
| 31 | +// Your other config settings... |
| 32 | +... |
| 33 | + |
| 34 | + build: { |
| 35 | + extensions: [ |
| 36 | + ffmpeg(), |
| 37 | + ], |
| 38 | + }, |
| 39 | +}); |
| 40 | +``` |
21 | 41 |
|
22 | 42 | ## Task code
|
23 | 43 |
|
24 | 44 | ```ts trigger/ffmpeg-compress-video.ts
|
25 |
| -import fetch from "node-fetch"; |
26 |
| -import fs from "fs/promises"; |
27 |
| -import path from "path"; |
| 45 | +import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; |
28 | 46 | import { logger, task } from "@trigger.dev/sdk/v3";
|
29 | 47 | import ffmpeg from "fluent-ffmpeg";
|
| 48 | +import fs from "fs/promises"; |
| 49 | +import fetch from "node-fetch"; |
| 50 | +import { Readable } from "node:stream"; |
| 51 | +import os from "os"; |
| 52 | +import path from "path"; |
| 53 | + |
| 54 | +// Initialize S3 client for R2 storage |
| 55 | +const s3Client = new S3Client({ |
| 56 | + region: "auto", |
| 57 | + endpoint: process.env.S3_ENDPOINT, |
| 58 | + credentials: { |
| 59 | + accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "", |
| 60 | + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "", |
| 61 | + }, |
| 62 | +}); |
30 | 63 |
|
31 | 64 | export const ffmpegCompressVideo = task({
|
32 | 65 | id: "ffmpeg-compress-video",
|
33 |
| - run: async (payload: { videoUrl: string }, { ctx }) => { |
34 |
| - if (ctx.environment.type !== "DEVELOPMENT") { |
35 |
| - ffmpeg.setFfmpegPath("/usr/bin/ffmpeg"); |
36 |
| - ffmpeg.setFfprobePath("/usr/bin/ffprobe"); |
37 |
| - } |
| 66 | + run: async (payload: { videoUrl: string }) => { |
38 | 67 | const { videoUrl } = payload;
|
39 | 68 |
|
40 |
| - // Generate temporary file names |
41 |
| - const tempInputPath = path.join("/tmp", `input_${Date.now()}.mp4`); |
42 |
| - const outputPath = path.join("/tmp", `output_${Date.now()}.mp4`); |
43 |
| - |
44 |
| - try { |
45 |
| - // Fetch the video |
46 |
| - const response = await fetch(videoUrl); |
47 |
| - const buffer = await response.buffer(); |
48 |
| - await fs.writeFile(tempInputPath, buffer); |
49 |
| - |
50 |
| - // Compress the video |
51 |
| - await new Promise((resolve, reject) => { |
52 |
| - ffmpeg(tempInputPath) |
53 |
| - .outputOptions([ |
54 |
| - "-c:v libx264", // Use H.264 codec |
55 |
| - "-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality) |
56 |
| - "-preset veryslow", // Slowest preset for best compression |
57 |
| - "-vf scale=iw/2:ih/2", // Reduce resolution to 320p width (height auto-calculated) |
58 |
| - "-c:a aac", // Use AAC for audio |
59 |
| - "-b:a 64k", // Reduce audio bitrate to 64k |
60 |
| - "-ac 1", // Convert to mono audio |
61 |
| - ]) |
62 |
| - .output(outputPath) |
63 |
| - .on("end", resolve) |
64 |
| - .on("error", reject) |
65 |
| - .run(); |
66 |
| - }); |
67 |
| - |
68 |
| - // Read the compressed video |
69 |
| - const compressedVideo = await fs.readFile(outputPath); |
70 |
| - |
71 |
| - // Get file sizes for comparison |
72 |
| - const originalSize = buffer.length; |
73 |
| - const compressedSize = compressedVideo.length; |
74 |
| - |
75 |
| - // Clean up temporary files |
76 |
| - await fs.unlink(tempInputPath); |
77 |
| - |
78 |
| - // Log compression results |
79 |
| - logger.log(`Original video size: ${originalSize} bytes`); |
80 |
| - logger.log(`Compressed video size: ${compressedSize} bytes`); |
81 |
| - logger.log(`Compression ratio: ${((1 - compressedSize / originalSize) * 100).toFixed(2)}%`); |
82 |
| - logger.log(`Compressed video saved at: ${outputPath}`); |
83 |
| - |
84 |
| - // Return the compressed video buffer, file path, and compression stats |
85 |
| - return { |
86 |
| - compressedVideoBuffer: compressedVideo, |
87 |
| - compressedVideoPath: outputPath, |
88 |
| - originalSize, |
89 |
| - compressedSize, |
90 |
| - compressionRatio: ((1 - compressedSize / originalSize) * 100).toFixed(2), |
91 |
| - }; |
92 |
| - } catch (error) { |
93 |
| - logger.error("Error compressing video:", { error }); |
94 |
| - // Clean up files in case of error |
95 |
| - try { |
96 |
| - await fs.unlink(tempInputPath); |
97 |
| - await fs.unlink(outputPath); |
98 |
| - } catch (cleanupError) { |
99 |
| - logger.error("Error during cleanup:", { cleanupError }); |
| 69 | + // Generate output file name with a timestamp |
| 70 | + const tempDirectory = os.tmpdir(); |
| 71 | + const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`); |
| 72 | + |
| 73 | + // Fetch the video from the provided URL |
| 74 | + const response = await fetch(videoUrl); |
| 75 | + |
| 76 | + // Compress the video using FFmpeg |
| 77 | + await new Promise((resolve, reject) => { |
| 78 | + if (!response.body) { |
| 79 | + return reject(new Error("Failed to fetch video")); |
100 | 80 | }
|
101 |
| - throw error; |
102 |
| - } |
| 81 | + |
| 82 | + ffmpeg(Readable.from(response.body)) |
| 83 | + .outputOptions([ |
| 84 | + "-c:v libx264", // Use H.264 codec |
| 85 | + "-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality) |
| 86 | + "-preset veryslow", // Slowest preset for best compression |
| 87 | + "-vf scale=iw/2:ih/2", // Reduce resolution to 50% of original width and height |
| 88 | + "-c:a aac", // Use AAC for audio |
| 89 | + "-b:a 64k", // Reduce audio bitrate to 64k |
| 90 | + "-ac 1", // Convert to mono audio |
| 91 | + ]) |
| 92 | + .output(outputPath) |
| 93 | + .on("end", resolve) |
| 94 | + .on("error", reject) |
| 95 | + .run(); |
| 96 | + }); |
| 97 | + |
| 98 | + // Read the compressed video into a buffer |
| 99 | + const compressedVideo = await fs.readFile(outputPath); |
| 100 | + |
| 101 | + // Get the compressed video size |
| 102 | + const compressedSize = compressedVideo.length; |
| 103 | + |
| 104 | + // Log compression results for debugging purposes |
| 105 | + logger.log(`Compressed video size: ${compressedSize} bytes`); |
| 106 | + logger.log(`Compressed video saved at: ${outputPath}`); |
| 107 | + |
| 108 | + // Generate the S3 key for the uploaded video file |
| 109 | + const s3Key = `processed-videos/${path.basename(outputPath)}`; |
| 110 | + |
| 111 | + // Set up the parameters for uploading the video to R2 |
| 112 | + const uploadParams = { |
| 113 | + Bucket: process.env.S3_BUCKET, |
| 114 | + Key: s3Key, |
| 115 | + Body: compressedVideo, |
| 116 | + }; |
| 117 | + |
| 118 | + // Upload the video to R2 and get the public URL |
| 119 | + await s3Client.send(new PutObjectCommand(uploadParams)); |
| 120 | + const s3Url = `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${s3Key}`; |
| 121 | + logger.log("Compressed video uploaded to R2", { url: s3Url }); |
| 122 | + |
| 123 | + // Delete the temporary compressed video file |
| 124 | + await fs.unlink(outputPath); |
| 125 | + |
| 126 | + // Return the compressed video file path, compressed size, and R2 URL |
| 127 | + return { |
| 128 | + compressedVideoPath: outputPath, |
| 129 | + compressedSize, |
| 130 | + s3Url, |
| 131 | + }; |
103 | 132 | },
|
104 | 133 | });
|
105 | 134 | ```
|
0 commit comments