Video processing pipeline with Trigger.dev and Ittybit

View Markdown

When a user uploads a video, you donโ€™t want to block the request while transcoding runs. Trigger.dev handles the background orchestration โ€” creating an Ittybit task, waiting for it to finish, generating thumbnails, and notifying the user. The whole pipeline runs reliably in the background with built-in retries.

Install dependencies

npm install @trigger.dev/sdk

Set your environment variables:

TRIGGER_SECRET_KEY=tr_dev_...
ITTYBIT_API_KEY=your_ittybit_api_key

Define the pipeline task

This Trigger.dev task accepts a video URL and runs the full processing pipeline as a series of steps.

import { task, wait } from '@trigger.dev/sdk/v3';

export const processVideo = task({
  id: 'process-video',
  retry: { maxAttempts: 3 },
  run: async (payload: { videoUrl: string; userId: string }) => {
    const { videoUrl, userId } = payload;

    // Step 1: Create transcoding task
    const transcodeTask = await createIttybitTask({
      input: videoUrl,
      kind: 'video',
      options: {
        width: 1920,
        format: 'mp4',
        codec: 'h264',
        quality: 'high',
      },
    });

    // Step 2: Poll until transcoding completes
    const completedTask = await pollForCompletion(transcodeTask.id);

    // Step 3: Generate thumbnail from the transcoded video
    const thumbnailTask = await createIttybitTask({
      input: completedTask.output_url,
      kind: 'image',
      options: {
        start: 2,
        width: 640,
        format: 'webp',
      },
    });

    const completedThumbnail = await pollForCompletion(thumbnailTask.id);

    // Step 4: Update your database and notify the user
    await updateVideoRecord(userId, {
      videoUrl: completedTask.output_url,
      thumbnailUrl: completedThumbnail.output_url,
      status: 'ready',
    });

    await notifyUser(userId, 'Your video is ready.');

    return {
      videoUrl: completedTask.output_url,
      thumbnailUrl: completedThumbnail.output_url,
    };
  },
});

Ittybit helper functions

These wrap the Ittybit Task API. createIttybitTask kicks off a processing job, and pollForCompletion waits for it to finish.

async function createIttybitTask(body: {
  input: string;
  kind: string;
  options: Record<string, unknown>;
}) {
  const res = await fetch('https://api.ittybit.com/jobs', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`Ittybit API error: ${res.status}`);
  }

  return res.json() as Promise<{
    id: string;
    status: string;
    output_url?: string;
  }>;
}

async function pollForCompletion(taskId: string, intervalMs = 5000, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    const res = await fetch(`https://api.ittybit.com/jobs/${taskId}`, {
      headers: {
        Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
      },
    });

    const task = (await res.json()) as {
      id: string;
      status: string;
      output_url?: string;
    };

    if (task.status === 'completed') {
      return task;
    }

    if (task.status === 'failed') {
      throw new Error(`Task ${taskId} failed`);
    }

    await new Promise((r) => setTimeout(r, intervalMs));
  }

  throw new Error(`Task ${taskId} timed out after polling`);
}

Trigger the task from your API

When a user uploads a video, trigger the background task and return immediately.

import { tasks } from "@trigger.dev/sdk/v3";
import { processVideo } from "./trigger/process-video";

// Next.js API route
export async function POST(req: Request) {
const { videoUrl, userId } = await req.json();

const handle = await tasks.trigger<typeof processVideo>(
"process-video",
{ videoUrl, userId },
);

return Response.json({
message: "Processing started",
runId: handle.id,
});
}
import { tasks } from "@trigger.dev/sdk/v3";
import { processVideo } from "./trigger/process-video";

app.post("/upload", async (req, res) => {
  const { videoUrl, userId } = req.body;

  const handle = await tasks.trigger<typeof processVideo>(
    "process-video",
    { videoUrl, userId },
  );

  res.json({
    message: "Processing started",
    runId: handle.id,
  });
});

Use webhooks instead of polling

Polling works but wastes compute. For production, configure an Ittybit webhook and use Trigger.devโ€™s wait.for to pause the task until the webhook fires.

import { task, wait } from '@trigger.dev/sdk/v3';

export const processVideoWithWebhooks = task({
  id: 'process-video-webhooks',
  retry: { maxAttempts: 3 },
  run: async (payload: { videoUrl: string; userId: string }) => {
    const { videoUrl, userId } = payload;

    // Create the transcode task
    const transcodeTask = await createIttybitTask({
      input: videoUrl,
      kind: 'video',
      options: {
        width: 1920,
        format: 'mp4',
        codec: 'h264',
        quality: 'high',
      },
    });

    // Wait for the webhook instead of polling
    const transcodeResult = await wait.for<{
      output_url: string;
    }>({
      id: `transcode-${transcodeTask.id}`,
      timeout: '30m',
    });

    // Generate thumbnail
    const thumbnailTask = await createIttybitTask({
      input: transcodeResult.output_url,
      kind: 'image',
      options: {
        start: 2,
        width: 640,
        format: 'webp',
      },
    });

    const thumbnailResult = await wait.for<{
      output_url: string;
    }>({
      id: `thumbnail-${thumbnailTask.id}`,
      timeout: '5m',
    });

    await updateVideoRecord(userId, {
      videoUrl: transcodeResult.output_url,
      thumbnailUrl: thumbnailResult.output_url,
      status: 'ready',
    });

    await notifyUser(userId, 'Your video is ready.');
  },
});

Then add a webhook endpoint that completes the wait token:

import { runs } from '@trigger.dev/sdk/v3';

// POST /api/webhooks/ittybit
export async function POST(req: Request) {
  const event = await req.json();

  if (event.type === 'job.succeeded') {
    await runs.completeWaitToken(`transcode-${event.task_id}`, { output_url: event.output_url });
  }

  return new Response('ok');
}

See also