Video processing pipeline with Trigger.dev and Ittybit
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');
}