Skip to content

Commit a84e601

Browse files
authored
Added new example tasks (FFmpeg / Sharp / Vercel AI SDK) (#1312)
* Added FFmpeg / sharp / vercel and updated mint.json * Amends including s3 -> r2
1 parent 9a3eab5 commit a84e601

File tree

5 files changed

+510
-50
lines changed

5 files changed

+510
-50
lines changed
+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
---
2+
title: "Video processing with FFmpeg"
3+
sidebarTitle: "FFmpeg video processing"
4+
description: "These examples show you how to process videos in various ways using FFmpeg with Trigger.dev."
5+
---
6+
7+
## Adding the FFmpeg build extension
8+
9+
To use these example tasks, you'll first need to add our FFmpeg extension to your project configuration like this:
10+
11+
```ts trigger.config.ts
12+
import { ffmpeg } from "@trigger.dev/build/extensions/core";
13+
import { defineConfig } from "@trigger.dev/sdk/v3";
14+
15+
export default defineConfig({
16+
project: "<project ref>",
17+
// Your other config settings...
18+
build: {
19+
extensions: [ffmpeg()],
20+
},
21+
});
22+
```
23+
24+
<Note>
25+
[Build extensions](../guides/build-extensions) allow you to hook into the build system and
26+
customize the build process or the resulting bundle and container image (in the case of
27+
deploying). You can use pre-built extensions or create your own.
28+
</Note>
29+
30+
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there.
31+
32+
## Compress a video using FFmpeg
33+
34+
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.
35+
36+
### Key Features:
37+
38+
- Fetches a video from a given URL
39+
- Compresses the video using FFmpeg with various compression settings
40+
- Uploads the compressed video to R2 storage
41+
42+
### Task code
43+
44+
```ts trigger/ffmpeg-compress-video.ts
45+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
46+
import { logger, task } from "@trigger.dev/sdk/v3";
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
55+
const s3Client = new S3Client({
56+
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
57+
region: "auto",
58+
endpoint: process.env.R2_ENDPOINT,
59+
credentials: {
60+
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
61+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
62+
},
63+
});
64+
65+
export const ffmpegCompressVideo = task({
66+
id: "ffmpeg-compress-video",
67+
run: async (payload: { videoUrl: string }) => {
68+
const { videoUrl } = payload;
69+
70+
// Generate temporary file names
71+
const tempDirectory = os.tmpdir();
72+
const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`);
73+
74+
// Fetch the video
75+
const response = await fetch(videoUrl);
76+
77+
// Compress the video
78+
await new Promise((resolve, reject) => {
79+
if (!response.body) {
80+
return reject(new Error("Failed to fetch video"));
81+
}
82+
83+
ffmpeg(Readable.from(response.body))
84+
.outputOptions([
85+
"-c:v libx264", // Use H.264 codec
86+
"-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality)
87+
"-preset veryslow", // Slowest preset for best compression
88+
"-vf scale=iw/2:ih/2", // Reduce resolution to 320p width (height auto-calculated)
89+
"-c:a aac", // Use AAC for audio
90+
"-b:a 64k", // Reduce audio bitrate to 64k
91+
"-ac 1", // Convert to mono audio
92+
])
93+
.output(outputPath)
94+
.on("end", resolve)
95+
.on("error", reject)
96+
.run();
97+
});
98+
99+
// Read the compressed video
100+
const compressedVideo = await fs.readFile(outputPath);
101+
102+
const compressedSize = compressedVideo.length;
103+
104+
// Log compression results
105+
logger.log(`Compressed video size: ${compressedSize} bytes`);
106+
logger.log(`Compressed video saved at: ${outputPath}`);
107+
108+
// Upload the compressed video to S3, replacing slashes with underscores
109+
const r2Key = `processed-videos/${path.basename(outputPath)}`;
110+
111+
const uploadParams = {
112+
Bucket: process.env.R2_BUCKET,
113+
Key: r2Key,
114+
Body: compressedVideo,
115+
};
116+
117+
// Upload the video to R2 and get the URL
118+
await s3Client.send(new PutObjectCommand(uploadParams));
119+
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
120+
logger.log("Compressed video uploaded to R2", { url: r2Url });
121+
122+
// Delete the temporary compressed video file
123+
await fs.unlink(outputPath);
124+
125+
// Return the compressed video file path, compressed size, and S3 URL
126+
return {
127+
compressedVideoPath: outputPath,
128+
compressedSize,
129+
r2Url,
130+
};
131+
},
132+
});
133+
```
134+
135+
## Extract audio from a video using FFmpeg
136+
137+
This task demonstrates how to use FFmpeg to extract audio from a video, convert it to WAV format, and upload it to R2 storage.
138+
139+
### Key Features:
140+
141+
- Fetches a video from a given URL
142+
- Extracts the audio from the video using FFmpeg
143+
- Converts the extracted audio to WAV format
144+
- Uploads the extracted audio to R2 storage
145+
146+
### Task code
147+
148+
<Warning>
149+
When testing, make sure to provide a video URL that contains audio. If the video does not have
150+
audio, the task will fail.
151+
</Warning>
152+
153+
```ts trigger/ffmpeg-extract-audio.ts
154+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
155+
import { logger, task } from "@trigger.dev/sdk/v3";
156+
import ffmpeg from "fluent-ffmpeg";
157+
import fs from "fs/promises";
158+
import fetch from "node-fetch";
159+
import { Readable } from "node:stream";
160+
import os from "os";
161+
import path from "path";
162+
163+
// Initialize S3 client
164+
const s3Client = new S3Client({
165+
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
166+
region: "auto",
167+
endpoint: process.env.R2_ENDPOINT,
168+
credentials: {
169+
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
170+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
171+
},
172+
});
173+
174+
export const ffmpegExtractAudio = task({
175+
id: "ffmpeg-extract-audio",
176+
run: async (payload: { videoUrl: string }) => {
177+
const { videoUrl } = payload;
178+
179+
// Generate temporary and output file names
180+
const tempDirectory = os.tmpdir();
181+
const outputPath = path.join(tempDirectory, `output_${Date.now()}.wav`);
182+
183+
// Fetch the video
184+
const response = await fetch(videoUrl);
185+
186+
// Convert the video to WAV
187+
await new Promise((resolve, reject) => {
188+
if (!response.body) {
189+
return reject(new Error("Failed to fetch video"));
190+
}
191+
ffmpeg(Readable.from(response.body))
192+
.toFormat("wav")
193+
.save(outputPath)
194+
.on("end", () => {
195+
logger.log(`WAV file saved to ${outputPath}`);
196+
resolve(outputPath);
197+
})
198+
.on("error", (err) => {
199+
reject(err);
200+
});
201+
});
202+
203+
// Read the WAV file
204+
const wavBuffer = await fs.readFile(outputPath);
205+
206+
// Log the output file path
207+
logger.log(`Converted video saved at: ${outputPath}`);
208+
209+
// Upload the compressed video to S3, replacing slashes with underscores
210+
const r2Key = `processed-audio/${path.basename(outputPath)}`;
211+
212+
const uploadParams = {
213+
Bucket: process.env.R2_BUCKET,
214+
Key: r2Key,
215+
Body: wavBuffer,
216+
};
217+
218+
// Upload the audio to R2 and get the URL
219+
await s3Client.send(new PutObjectCommand(uploadParams));
220+
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
221+
logger.log("Extracted audio uploaded to R2", { url: r2Url });
222+
223+
// Delete the temporary file
224+
await fs.unlink(outputPath);
225+
226+
// Return the WAV buffer and file path
227+
return {
228+
wavBuffer,
229+
wavFilePath: outputPath,
230+
r2Url,
231+
};
232+
},
233+
});
234+
```
235+
236+
## Generate a thumbnail from a video using FFmpeg
237+
238+
This task demonstrates how to use FFmpeg to generate a thumbnail from a video at a specific time and upload the generated thumbnail to R2 storage.
239+
240+
### Key Features:
241+
242+
- Fetches a video from a given URL
243+
- Generates a thumbnail from the video at the 5-second mark
244+
- Uploads the generated thumbnail to R2 storage
245+
246+
### Task code
247+
248+
```ts trigger/ffmpeg-generate-thumbnail.ts
249+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
250+
import { logger, task } from "@trigger.dev/sdk/v3";
251+
import ffmpeg from "fluent-ffmpeg";
252+
import fs from "fs/promises";
253+
import fetch from "node-fetch";
254+
import { Readable } from "node:stream";
255+
import os from "os";
256+
import path from "path";
257+
258+
// Initialize S3 client
259+
const s3Client = new S3Client({
260+
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/
261+
region: "auto",
262+
endpoint: process.env.R2_ENDPOINT,
263+
credentials: {
264+
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
265+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
266+
},
267+
});
268+
269+
export const ffmpegGenerateThumbnail = task({
270+
id: "ffmpeg-generate-thumbnail",
271+
run: async (payload: { videoUrl: string }) => {
272+
const { videoUrl } = payload;
273+
274+
// Generate output file name
275+
const tempDirectory = os.tmpdir();
276+
const outputPath = path.join(tempDirectory, `thumbnail_${Date.now()}.jpg`);
277+
278+
// Fetch the video
279+
const response = await fetch(videoUrl);
280+
281+
// Generate the thumbnail
282+
await new Promise((resolve, reject) => {
283+
if (!response.body) {
284+
return reject(new Error("Failed to fetch video"));
285+
}
286+
ffmpeg(Readable.from(response.body))
287+
.screenshots({
288+
count: 1,
289+
folder: "/tmp",
290+
filename: path.basename(outputPath),
291+
size: "320x240",
292+
timemarks: ["5"], // 5 seconds
293+
})
294+
.on("end", resolve)
295+
.on("error", reject);
296+
});
297+
298+
// Read the generated thumbnail
299+
const thumbnail = await fs.readFile(outputPath);
300+
301+
// Upload the compressed video to S3, replacing slashes with underscores
302+
const r2Key = `thumbnails/${path.basename(outputPath)}`;
303+
304+
const uploadParams = {
305+
Bucket: process.env.R2_BUCKET,
306+
Key: r2Key,
307+
Body: thumbnail,
308+
};
309+
310+
// Upload the thumbnail to R2 and get the URL
311+
await s3Client.send(new PutObjectCommand(uploadParams));
312+
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
313+
logger.log("Thumbnail uploaded to R2", { url: r2Url });
314+
315+
// Delete the temporary file
316+
await fs.unlink(outputPath);
317+
318+
// Log thumbnail generation results
319+
logger.log(`Thumbnail uploaded to S3: ${r2Url}`);
320+
321+
// Return the thumbnail buffer, file path, sizes, and S3 URL
322+
return {
323+
thumbnailBuffer: thumbnail,
324+
thumbnailPath: outputPath,
325+
r2Url,
326+
};
327+
},
328+
});
329+
```

0 commit comments

Comments
 (0)