Prerequisites
Build configuration
Add the FFmpeg build extension to your trigger.config.ts. This ensures FFmpeg is available in
the deployed container:
import { ffmpeg } from "@trigger.dev/build/extensions/core";
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
project: "<project ref>",
build: {
extensions: [ffmpeg()],
},
});
Build extensions customize the build process and the container
image used when deploying tasks. You’ll also need @trigger.dev/build in your devDependencies.
If you use fluent-ffmpeg, add it to external in your config to
prevent it from being bundled:
export default defineConfig({
project: "<project ref>",
build: {
extensions: [ffmpeg()],
external: ["fluent-ffmpeg"],
},
});
Example 1: Compress a video
This task fetches a video from a URL, compresses it using H.264 with reduced resolution and
bitrate, then uploads the result to Cloudflare R2.
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";
const s3Client = new S3Client({
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;
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `output_${Date.now()}.mp4`);
const response = await fetch(videoUrl);
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", // H.264 codec
"-crf 28", // Higher CRF = more compression
"-preset veryslow", // Slowest preset = best compression ratio
"-vf scale=iw/2:ih/2", // Halve the resolution
"-c:a aac", // AAC audio codec
"-b:a 64k", // 64 kbps audio bitrate
"-ac 1", // Mono audio
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
const compressedVideo = await fs.readFile(outputPath);
logger.log(`Compressed video size: ${compressedVideo.length} bytes`);
const r2Key = `processed-videos/${path.basename(outputPath)}`;
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: compressedVideo,
})
);
logger.log(`Uploaded to R2`, { r2Key });
await fs.unlink(outputPath);
return { Bucket: process.env.R2_BUCKET, r2Key };
},
});
Test payload:
{ "videoUrl": "https://example.com/your-video.mp4" }
This task extracts the audio track from a video, converts it to PCM WAV format, and uploads it
to R2.
The video must contain an audio track. If it doesn’t, the task will fail at the FFmpeg step.
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";
const s3Client = new S3Client({
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;
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `audio_${Date.now()}.wav`);
const response = await fetch(videoUrl);
await new Promise((resolve, reject) => {
if (!response.body) return reject(new Error("Failed to fetch video"));
ffmpeg(Readable.from(response.body))
.outputOptions([
"-vn", // No video output
"-acodec pcm_s16le", // PCM 16-bit little-endian
"-ar 44100", // 44.1 kHz sample rate
"-ac 2", // Stereo
])
.output(outputPath)
.on("end", resolve)
.on("error", reject)
.run();
});
const audioBuffer = await fs.readFile(outputPath);
logger.log(`Extracted audio size: ${audioBuffer.length} bytes`);
const r2Key = `extracted-audio/${path.basename(outputPath)}`;
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: audioBuffer,
})
);
logger.log(`Uploaded to R2`, { r2Key });
await fs.unlink(outputPath);
return { Bucket: process.env.R2_BUCKET, r2Key };
},
});
Test payload:
{ "videoUrl": "https://example.com/your-video-with-audio.mp4" }
Example 3: Generate a thumbnail
This task captures a single frame from the 5-second mark of a video and uploads the JPEG
thumbnail to R2.
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";
const s3Client = new S3Client({
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;
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `thumbnail_${Date.now()}.jpg`);
const response = await fetch(videoUrl);
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"], // capture at 5 seconds
})
.on("end", resolve)
.on("error", reject);
});
const thumbnail = await fs.readFile(outputPath);
const r2Key = `thumbnails/${path.basename(outputPath)}`;
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: r2Key,
Body: thumbnail,
})
);
const r2Url = `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}/${r2Key}`;
logger.log("Thumbnail uploaded to R2", { url: r2Url });
await fs.unlink(outputPath);
return { thumbnail, thumbnailPath: outputPath, r2Url };
},
});
Test payload:
{ "videoUrl": "https://example.com/your-video.mp4" }
Local development
When running locally with trigger.dev dev, build extensions are not applied. You need FFmpeg
installed directly on your machine:
macOS
Ubuntu/Debian
Windows
sudo apt-get install ffmpeg
Download the FFmpeg build from ffmpeg.org and add it to
your PATH.