chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
import LocalDevelopment from "/snippets/local-development-extensions.mdx";
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A project with [Trigger.dev initialized](/quick-start)
|
||||
- [FFmpeg](https://www.ffmpeg.org/download.html) installed on your machine
|
||||
|
||||
### 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";
|
||||
|
||||
export default defineConfig({
|
||||
project: "<project ref>",
|
||||
// Your other config settings...
|
||||
build: {
|
||||
extensions: [ffmpeg()],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
[Build extensions](/config/extensions/overview) 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.
|
||||
|
||||
If you are modifying this example and using popular FFmpeg libraries like `fluent-ffmpeg` you'll also need to add them to [`external`](/config/config-file#external) in your `trigger.config.ts` file.
|
||||
|
||||
## 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";
|
||||
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 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(`Temporary compressed video file created`, { outputPath });
|
||||
|
||||
// Create the r2Key for the extracted audio, using the base name of the output path
|
||||
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));
|
||||
logger.log(`Compressed video saved to your r2 bucket`, { r2Key });
|
||||
|
||||
// Delete the temporary compressed video file
|
||||
await fs.unlink(outputPath);
|
||||
logger.log(`Temporary compressed video file deleted`, { outputPath });
|
||||
|
||||
// Return the compressed video buffer and r2 key
|
||||
return {
|
||||
Bucket: process.env.R2_BUCKET,
|
||||
r2Key,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Testing your task
|
||||
|
||||
To test this task, use this payload structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```ts trigger/ffmpeg-extract-audio.ts
|
||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { logger, task } from "@trigger.dev/sdk";
|
||||
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 file names
|
||||
const tempDirectory = os.tmpdir();
|
||||
const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`);
|
||||
|
||||
// Fetch the video
|
||||
const response = await fetch(videoUrl);
|
||||
|
||||
// Extract the audio
|
||||
await new Promise((resolve, reject) => {
|
||||
if (!response.body) {
|
||||
return reject(new Error("Failed to fetch video"));
|
||||
}
|
||||
|
||||
ffmpeg(Readable.from(response.body))
|
||||
.outputOptions([
|
||||
"-vn", // Disable video output
|
||||
"-acodec pcm_s16le", // Use PCM 16-bit little-endian encoding
|
||||
"-ar 44100", // Set audio sample rate to 44.1 kHz
|
||||
"-ac 2", // Set audio channels to stereo
|
||||
])
|
||||
.output(outputPath)
|
||||
.on("end", resolve)
|
||||
.on("error", reject)
|
||||
.run();
|
||||
});
|
||||
|
||||
// Read the extracted audio
|
||||
const audioBuffer = await fs.readFile(outputPath);
|
||||
const audioSize = audioBuffer.length;
|
||||
|
||||
// Log audio extraction results
|
||||
logger.log(`Extracted audio size: ${audioSize} bytes`);
|
||||
logger.log(`Temporary audio file created`, { outputPath });
|
||||
|
||||
// Create the r2Key for the extracted audio, using the base name of the output path
|
||||
const r2Key = `extracted-audio/${path.basename(outputPath)}`;
|
||||
|
||||
const uploadParams = {
|
||||
Bucket: process.env.R2_BUCKET,
|
||||
Key: r2Key,
|
||||
Body: audioBuffer,
|
||||
};
|
||||
|
||||
// Upload the audio to R2 and get the URL
|
||||
await s3Client.send(new PutObjectCommand(uploadParams));
|
||||
logger.log(`Extracted audio saved to your R2 bucket`, { r2Key });
|
||||
|
||||
// Delete the temporary audio file
|
||||
await fs.unlink(outputPath);
|
||||
logger.log(`Temporary audio file deleted`, { outputPath });
|
||||
|
||||
// Return the audio file path, size, and R2 URL
|
||||
return {
|
||||
Bucket: process.env.R2_BUCKET,
|
||||
r2Key,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Testing your task
|
||||
|
||||
To test this task, use this payload structure:
|
||||
|
||||
<Warning>
|
||||
Make sure to provide a video URL that contains audio. If the video does not have audio, the task
|
||||
will fail.
|
||||
</Warning>
|
||||
|
||||
```json
|
||||
{
|
||||
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
|
||||
}
|
||||
```
|
||||
|
||||
## 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";
|
||||
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);
|
||||
|
||||
// Create the r2Key for the extracted audio, using the base name of the output path
|
||||
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, path, and R2 URL
|
||||
return {
|
||||
thumbnailBuffer: thumbnail,
|
||||
thumbnailPath: outputPath,
|
||||
r2Url,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Testing your task
|
||||
|
||||
To test this task in the dashboard, you can use the following payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
|
||||
}
|
||||
```
|
||||
|
||||
<LocalDevelopment packages={"ffmpeg"} />
|
||||
Reference in New Issue
Block a user