Skip to main content

Prerequisites

Build configuration

Add the FFmpeg build extension to your trigger.config.ts. This ensures FFmpeg is available in the deployed container:
trigger.config.ts
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:
trigger.config.ts
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" }

Example 2: Extract audio from a video

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:
brew install ffmpeg

Build docs developers (and LLMs) love