AWS event-driven media processing with Ittybit
Drop a file into S3 and walk away. EventBridge picks up the notification, triggers a Lambda that kicks off Ittybit processing, and a webhook callback writes the result to DynamoDB. Zero polling, fully serverless.
Architecture
S3 upload -> EventBridge rule -> Lambda (dispatch) -> Ittybit API
|
DynamoDB <- Lambda (webhook) <- API Gateway <- Ittybit webhook
Prerequisites
- An S3 bucket with EventBridge notifications enabled
- An Ittybit connection configured for your bucket
ITTYBIT_API_KEYstored in AWS Secrets Manager (or SSM Parameter Store)
Enable EventBridge on your bucket
aws s3api put-bucket-notification-configuration \
--bucket my-media-bucket \
--notification-configuration '{"EventBridgeConfiguration": {}}'
EventBridge rule
Match PutObject events for media files:
{
"source": ["aws.s3"],
"detail-type": ["Object Created"],
"detail": {
"bucket": { "name": ["my-media-bucket"] },
"object": {
"key": [
{ "suffix": ".mov" },
{ "suffix": ".mp4" },
{ "suffix": ".mkv" },
{ "suffix": ".webm" }
]
}
}
}
Target this rule at your dispatch Lambda.
Dispatch Lambda
The handler extracts the bucket and key from the EventBridge event, then POSTs a task to Ittybit.
import { EventBridgeEvent } from "aws-lambda";
const ITTYBIT_API_KEY = process.env.ITTYBIT_API_KEY!;
const WEBHOOK_URL = process.env.WEBHOOK_URL!; // your API Gateway / Function URL
const CONNECTION_ID = process.env.ITTYBIT_CONNECTION_ID!;
interface S3ObjectCreated {
bucket: { name: string };
object: { key: string; size: number };
}
export const handler = async (event: EventBridgeEvent<"Object Created", S3ObjectCreated>) => {
const { bucket, object } = event.detail;
const input = `s3://${bucket.name}/${object.key}`;
const task = {
input,
kind: "video",
options: {
connection_id: CONNECTION_ID,
width: 1920,
format: "mp4",
quality: "high",
},
webhook_url: WEBHOOK_URL,
metadata: {
source_bucket: bucket.name,
source_key: object.key,
},
};
const res = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(task),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Ittybit API error ${res.status}: ${body}`);
}
const data = await res.json();
console.log("Task created:", data.id);
return data;
};
import json
import os
import urllib.request
ITTYBIT_API_KEY = os.environ["ITTYBIT_API_KEY"]
WEBHOOK_URL = os.environ["WEBHOOK_URL"]
CONNECTION_ID = os.environ["ITTYBIT_CONNECTION_ID"]
def handler(event, context):
detail = event["detail"]
bucket = detail["bucket"]["name"]
key = detail["object"]["key"]
input_url = f"s3://{bucket}/{key}"
task = {
"input": input_url,
"kind": "video",
"options": {
"connection_id": CONNECTION_ID,
"width": 1920,
"format": "mp4",
"quality": "high",
},
"webhook_url": WEBHOOK_URL,
"metadata": {
"source_bucket": bucket,
"source_key": key,
},
}
req = urllib.request.Request(
"https://api.ittybit.com/jobs",
data=json.dumps(task).encode(),
headers={
"Authorization": f"Bearer {ITTYBIT_API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req) as res:
data = json.loads(res.read())
print("Task created:", data["id"])
return data# Manual test -- simulate what the Lambda does
TASK='{
"input": "s3://my-media-bucket/uploads/video.mov",
"kind": "video",
"options": {
"connection_id": "conn_abc123",
"width": 1920,
"format": "mp4",
"quality": "high"
},
"webhook_url": "https://xyz.lambda-url.us-east-1.on.aws/webhook",
"metadata": {
"source_bucket": "my-media-bucket",
"source_key": "uploads/video.mov"
}
}'
curl -X POST https://api.ittybit.com/jobs \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d "$TASK" Webhook Lambda
Ittybit POSTs to your webhook_url when the task completes. This handler writes the result to DynamoDB.
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
const db = new DynamoDBClient({});
const TABLE_NAME = process.env.TABLE_NAME!;
export const handler = async (event: { body: string }) => {
const payload = JSON.parse(event.body);
const task = payload.data;
await db.send(
new PutItemCommand({
TableName: TABLE_NAME,
Item: marshall({
pk: task.metadata.source_key,
sk: `task#${task.id}`,
status: task.status,
kind: task.kind,
output_url: task.output?.url,
duration_ms: task.duration_ms,
created_at: task.created_at,
completed_at: job.succeeded_at,
}),
})
);
console.log(`Stored result for ${task.id}: ${task.status}`);
return { statusCode: 200, body: "ok" };
};
import json
import os
import boto3
TABLE_NAME = os.environ["TABLE_NAME"]
table = boto3.resource("dynamodb").Table(TABLE_NAME)
def handler(event, context):
payload = json.loads(event["body"])
task = payload["data"]
table.put_item(
Item={
"pk": task["metadata"]["source_key"],
"sk": f"task#{task['id']}",
"status": task["status"],
"kind": task["kind"],
"output_url": task.get("output", {}).get("url"),
"duration_ms": task.get("duration_ms"),
"created_at": task["created_at"],
"completed_at": task.get("completed_at"),
}
)
print(f"Stored result for {task['id']}: {task['status']}")
return {"statusCode": 200, "body": "ok"}# Example webhook payload from Ittybit
{
"event": "job.succeeded",
"data": {
"id": "task_abc123",
"status": "completed",
"kind": "video",
"input": "s3://my-media-bucket/uploads/video.mov",
"output": {
"url": "https://cdn.ittybit.com/abc/video.mp4"
},
"duration_ms": 4820,
"metadata": {
"source_bucket": "my-media-bucket",
"source_key": "uploads/video.mov"
},
"created_at": 1775210400000,
"completed_at": 1775210404000
}
} Multiple task types per upload
Kick off several tasks in parallel from the same dispatch Lambda β video transcode, thumbnail, and audio extraction:
const tasks = [
{ input, kind: 'video', options: { connection_id: CONNECTION_ID, width: 1920, format: 'mp4' } },
{
input,
kind: 'image',
options: { connection_id: CONNECTION_ID, width: 640, format: 'webp', start: 2 },
},
{
input,
kind: 'audio',
options: { connection_id: CONNECTION_ID, format: 'mp3', quality: 'medium' },
},
];
await Promise.all(
tasks.map((task) =>
fetch('https://api.ittybit.com/jobs', {
method: 'POST',
headers: {
Authorization: `Bearer ${ITTYBIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...task,
webhook_url: WEBHOOK_URL,
metadata: { source_bucket: bucket.name, source_key: object.key },
}),
}),
),
);
Each task fires its own webhook on completion, so your webhook Lambda handles them independently.
See also
- Process files from S3 β setting up S3 connections
- Write output to S3 β writing processed files back to your bucket
- Build a user upload pipeline β multi-step processing patterns