-
-
Notifications
You must be signed in to change notification settings - Fork 725
Added new example tasks (FFmpeg / Sharp / Vercel AI SDK) #1312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,329 @@ | ||
--- | ||
title: "Video processing with FFmpeg" | ||
sidebarTitle: "FFmpeg video processing" | ||
description: "These examples show you how to process videos in various ways using FFmpeg with Trigger.dev." | ||
--- | ||
|
||
## Adding the FFmpeg build extension | ||
|
||
To use these example tasks, you'll first need to add our FFmpeg extension to your project configuration like this: | ||
|
||
```ts trigger.config.ts | ||
import { ffmpeg } from "@trigger.dev/build/extensions/core"; | ||
import { defineConfig } from "@trigger.dev/sdk/v3"; | ||
|
||
export default defineConfig({ | ||
project: "<project ref>", | ||
// Your other config settings... | ||
build: { | ||
extensions: [ffmpeg()], | ||
}, | ||
}); | ||
``` | ||
|
||
<Note> | ||
[Build extensions](../guides/build-extensions) allow you to hook into the build system and | ||
customize the build process or the resulting bundle and container image (in the case of | ||
deploying). You can use pre-built extensions or create your own. | ||
</Note> | ||
|
||
You'll also need to add `@trigger.dev/build` to your `package.json` file under `devDependencies` if you don't already have it there. | ||
|
||
## Compress a video using FFmpeg | ||
|
||
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. | ||
|
||
### Key Features: | ||
|
||
- Fetches a video from a given URL | ||
- Compresses the video using FFmpeg with various compression settings | ||
- Uploads the compressed video to R2 storage | ||
|
||
### Task code | ||
|
||
```ts trigger/ffmpeg-compress-video.ts | ||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; | ||
import { logger, task } from "@trigger.dev/sdk/v3"; | ||
import ffmpeg from "fluent-ffmpeg"; | ||
import fs from "fs/promises"; | ||
import fetch from "node-fetch"; | ||
import { Readable } from "node:stream"; | ||
import os from "os"; | ||
import path from "path"; | ||
|
||
// Initialize S3 client | ||
const s3Client = new S3Client({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd link to this document about how to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/ |
||
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/ | ||
region: "auto", | ||
endpoint: process.env.R2_ENDPOINT, | ||
credentials: { | ||
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "", | ||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "", | ||
}, | ||
}); | ||
|
||
export const ffmpegCompressVideo = task({ | ||
id: "ffmpeg-compress-video", | ||
run: async (payload: { videoUrl: string }) => { | ||
const { videoUrl } = payload; | ||
|
||
// Generate temporary file names | ||
const tempDirectory = os.tmpdir(); | ||
const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`); | ||
|
||
// Fetch the video | ||
const response = await fetch(videoUrl); | ||
|
||
// Compress the video | ||
await new Promise((resolve, reject) => { | ||
if (!response.body) { | ||
return reject(new Error("Failed to fetch video")); | ||
} | ||
|
||
ffmpeg(Readable.from(response.body)) | ||
.outputOptions([ | ||
"-c:v libx264", // Use H.264 codec | ||
"-crf 28", // Higher CRF for more compression (28 is near the upper limit for acceptable quality) | ||
"-preset veryslow", // Slowest preset for best compression | ||
"-vf scale=iw/2:ih/2", // Reduce resolution to 320p width (height auto-calculated) | ||
"-c:a aac", // Use AAC for audio | ||
"-b:a 64k", // Reduce audio bitrate to 64k | ||
"-ac 1", // Convert to mono audio | ||
]) | ||
.output(outputPath) | ||
.on("end", resolve) | ||
.on("error", reject) | ||
.run(); | ||
}); | ||
|
||
// Read the compressed video | ||
const compressedVideo = await fs.readFile(outputPath); | ||
|
||
const compressedSize = compressedVideo.length; | ||
|
||
// Log compression results | ||
logger.log(`Compressed video size: ${compressedSize} bytes`); | ||
logger.log(`Compressed video saved at: ${outputPath}`); | ||
|
||
// Upload the compressed video to S3, replacing slashes with underscores | ||
const r2Key = `processed-videos/${path.basename(outputPath)}`; | ||
|
||
const uploadParams = { | ||
Bucket: process.env.R2_BUCKET, | ||
Key: r2Key, | ||
Body: compressedVideo, | ||
}; | ||
|
||
// Upload the video to R2 and get the URL | ||
await s3Client.send(new PutObjectCommand(uploadParams)); | ||
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`; | ||
logger.log("Compressed video uploaded to R2", { url: r2Url }); | ||
|
||
// Delete the temporary compressed video file | ||
await fs.unlink(outputPath); | ||
|
||
// Return the compressed video file path, compressed size, and S3 URL | ||
return { | ||
compressedVideoPath: outputPath, | ||
compressedSize, | ||
r2Url, | ||
}; | ||
}, | ||
}); | ||
``` | ||
|
||
## Extract audio from a video using FFmpeg | ||
|
||
This task demonstrates how to use FFmpeg to extract audio from a video, convert it to WAV format, and upload it to R2 storage. | ||
|
||
### Key Features: | ||
|
||
- Fetches a video from a given URL | ||
- Extracts the audio from the video using FFmpeg | ||
- Converts the extracted audio to WAV format | ||
- Uploads the extracted audio to R2 storage | ||
|
||
### Task code | ||
|
||
<Warning> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's give people example URLs they can use |
||
When testing, make sure to provide a video URL that contains audio. If the video does not have | ||
audio, the task will fail. | ||
</Warning> | ||
|
||
```ts trigger/ffmpeg-extract-audio.ts | ||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; | ||
import { logger, task } from "@trigger.dev/sdk/v3"; | ||
import ffmpeg from "fluent-ffmpeg"; | ||
import fs from "fs/promises"; | ||
import fetch from "node-fetch"; | ||
import { Readable } from "node:stream"; | ||
import os from "os"; | ||
import path from "path"; | ||
|
||
// Initialize S3 client | ||
const s3Client = new S3Client({ | ||
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/ | ||
region: "auto", | ||
endpoint: process.env.R2_ENDPOINT, | ||
credentials: { | ||
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "", | ||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "", | ||
}, | ||
}); | ||
|
||
export const ffmpegExtractAudio = task({ | ||
id: "ffmpeg-extract-audio", | ||
run: async (payload: { videoUrl: string }) => { | ||
const { videoUrl } = payload; | ||
|
||
// Generate temporary and output file names | ||
const tempDirectory = os.tmpdir(); | ||
const outputPath = path.join(tempDirectory, `output_${Date.now()}.wav`); | ||
|
||
// Fetch the video | ||
const response = await fetch(videoUrl); | ||
|
||
// Convert the video to WAV | ||
await new Promise((resolve, reject) => { | ||
if (!response.body) { | ||
return reject(new Error("Failed to fetch video")); | ||
} | ||
ffmpeg(Readable.from(response.body)) | ||
.toFormat("wav") | ||
.save(outputPath) | ||
.on("end", () => { | ||
logger.log(`WAV file saved to ${outputPath}`); | ||
resolve(outputPath); | ||
}) | ||
.on("error", (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
|
||
// Read the WAV file | ||
const wavBuffer = await fs.readFile(outputPath); | ||
|
||
// Log the output file path | ||
logger.log(`Converted video saved at: ${outputPath}`); | ||
|
||
// Upload the compressed video to S3, replacing slashes with underscores | ||
const r2Key = `processed-audio/${path.basename(outputPath)}`; | ||
|
||
const uploadParams = { | ||
Bucket: process.env.R2_BUCKET, | ||
Key: r2Key, | ||
Body: wavBuffer, | ||
}; | ||
|
||
// Upload the audio to R2 and get the URL | ||
await s3Client.send(new PutObjectCommand(uploadParams)); | ||
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`; | ||
logger.log("Extracted audio uploaded to R2", { url: r2Url }); | ||
|
||
// Delete the temporary file | ||
await fs.unlink(outputPath); | ||
|
||
// Return the WAV buffer and file path | ||
return { | ||
wavBuffer, | ||
wavFilePath: outputPath, | ||
r2Url, | ||
}; | ||
}, | ||
}); | ||
``` | ||
|
||
## Generate a thumbnail from a video using FFmpeg | ||
|
||
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. | ||
|
||
### Key Features: | ||
|
||
- Fetches a video from a given URL | ||
- Generates a thumbnail from the video at the 5-second mark | ||
- Uploads the generated thumbnail to R2 storage | ||
|
||
### Task code | ||
|
||
```ts trigger/ffmpeg-generate-thumbnail.ts | ||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; | ||
import { logger, task } from "@trigger.dev/sdk/v3"; | ||
import ffmpeg from "fluent-ffmpeg"; | ||
import fs from "fs/promises"; | ||
import fetch from "node-fetch"; | ||
import { Readable } from "node:stream"; | ||
import os from "os"; | ||
import path from "path"; | ||
|
||
// Initialize S3 client | ||
const s3Client = new S3Client({ | ||
// How to authenticate to R2: https://developers.cloudflare.com/r2/api/s3/tokens/ | ||
region: "auto", | ||
endpoint: process.env.R2_ENDPOINT, | ||
credentials: { | ||
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "", | ||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "", | ||
}, | ||
}); | ||
|
||
export const ffmpegGenerateThumbnail = task({ | ||
id: "ffmpeg-generate-thumbnail", | ||
run: async (payload: { videoUrl: string }) => { | ||
const { videoUrl } = payload; | ||
|
||
// Generate output file name | ||
const tempDirectory = os.tmpdir(); | ||
const outputPath = path.join(tempDirectory, `thumbnail_${Date.now()}.jpg`); | ||
|
||
// Fetch the video | ||
const response = await fetch(videoUrl); | ||
|
||
// Generate the thumbnail | ||
await new Promise((resolve, reject) => { | ||
if (!response.body) { | ||
return reject(new Error("Failed to fetch video")); | ||
} | ||
ffmpeg(Readable.from(response.body)) | ||
.screenshots({ | ||
count: 1, | ||
folder: "/tmp", | ||
filename: path.basename(outputPath), | ||
size: "320x240", | ||
timemarks: ["5"], // 5 seconds | ||
}) | ||
.on("end", resolve) | ||
.on("error", reject); | ||
}); | ||
|
||
// Read the generated thumbnail | ||
const thumbnail = await fs.readFile(outputPath); | ||
|
||
// Upload the compressed video to S3, replacing slashes with underscores | ||
const r2Key = `thumbnails/${path.basename(outputPath)}`; | ||
|
||
const uploadParams = { | ||
Bucket: process.env.R2_BUCKET, | ||
Key: r2Key, | ||
Body: thumbnail, | ||
}; | ||
|
||
// Upload the thumbnail to R2 and get the URL | ||
await s3Client.send(new PutObjectCommand(uploadParams)); | ||
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`; | ||
logger.log("Thumbnail uploaded to R2", { url: r2Url }); | ||
|
||
// Delete the temporary file | ||
await fs.unlink(outputPath); | ||
|
||
// Log thumbnail generation results | ||
logger.log(`Thumbnail uploaded to S3: ${r2Url}`); | ||
|
||
// Return the thumbnail buffer, file path, sizes, and S3 URL | ||
return { | ||
thumbnailBuffer: thumbnail, | ||
thumbnailPath: outputPath, | ||
r2Url, | ||
}; | ||
}, | ||
}); | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably mention the need to add
@trigger.dev/build
to yourdevDependencies
if it already isn't there (init
will now add it).