Video upload pipeline with Laravel and Ittybit
Users upload a video through a Laravel form, you store it in S3 via the Storage facade, then hand it off to Ittybit for processing. When the task finishes, a webhook hits your app and you update the media record with the output URL.
Migration and model
Create a videos table to track uploads and their processed output.
php artisan make:model Video -m
// database/migrations/xxxx_create_videos_table.php
Schema::create('videos', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('original_path');
$table->string('ittybit_task_id')->nullable();
$table->string('status')->default('uploading');
$table->string('output_url')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
// app/Models/Video.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
{
protected $fillable = [
'user_id',
'original_path',
'ittybit_task_id',
'status',
'output_url',
'metadata',
];
protected $casts = [
'metadata' => 'array',
];
}
Upload controller
Accept the file, store it to S3, and dispatch the Ittybit task.
// app/Http/Controllers/VideoController.php
namespace App\Http\Controllers;
use App\Models\Video;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
class VideoController extends Controller
{
public function store(Request $request)
{
$request->validate([
'video' => 'required|file|mimetypes:video/*|max:512000',
]);
$path = $request->file('video')->store('uploads', 's3');
$video = Video::create([
'user_id' => $request->user()->id,
'original_path' => $path,
'status' => 'processing',
]);
$s3Url = 's3://' . config('filesystems.disks.s3.bucket') . '/' . $path;
$response = Http::withToken(config('services.ittybit.api_key'))
->post('https://api.ittybit.com/jobs', [
'input' => $s3Url,
'connection_id' => config('services.ittybit.connection_id'),
'kind' => 'video',
'options' => [
'width' => 1920,
'format' => 'mp4',
'quality' => 'high',
],
]);
$video->update([
'ittybit_task_id' => $response->json('id'),
]);
return back()->with('status', 'Video uploaded and processing.');
}
}
Add credentials to your .env:
ITTYBIT_API_KEY=your_ittybit_api_key
ITTYBIT_CONNECTION_ID=conn_xxxxx
ITTYBIT_WEBHOOK_SECRET=your_webhook_secret
And register them in config:
// config/services.php
'ittybit' => [
'api_key' => env('ITTYBIT_API_KEY'),
'connection_id' => env('ITTYBIT_CONNECTION_ID'),
'webhook_secret' => env('ITTYBIT_WEBHOOK_SECRET'),
],
Webhook route
Ittybit POSTs to your app when the task completes. Register a route that skips CSRF verification and updates the video record.
// routes/api.php
use App\Http\Controllers\WebhookController;
Route::post('/webhooks/ittybit', [WebhookController::class, 'handle']);
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use App\Models\Video;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$secret = config('services.ittybit.webhook_secret');
$signature = $request->header('Ittybit-Signature');
if (!hash_equals(hash_hmac('sha256', $request->getContent(), $secret), $signature)) {
abort(401);
}
$payload = $request->all();
$video = Video::where('ittybit_task_id', $payload['id'])->first();
if (!$video) {
return response()->json(['status' => 'ignored'], 200);
}
if ($payload['status'] === 'completed') {
$video->update([
'status' => 'completed',
'output_url' => $payload['output']['url'],
'metadata' => [
'duration' => $payload['output']['duration'],
'width' => $payload['output']['width'],
'height' => $payload['output']['height'],
'format' => $payload['output']['format'],
],
]);
} else {
$video->update(['status' => 'failed']);
}
return response()->json(['status' => 'ok'], 200);
}
}
Upload form
A minimal Blade template with the file input.
{{-- resources/views/videos/upload.blade.php --}}
<form action="{{ route('videos.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="video" accept="video/*" required>
<button type="submit">Upload</button>
</form>
S3 connection setup
Before dispatching tasks with s3:// URLs, create a connection in Ittybit so it can read from your bucket.
ittybit connections add s3 \
--name my-uploads \
--endpoint https://s3.us-east-1.amazonaws.com \
--region us-east-1 \
--access-key-id $AWS_ACCESS_KEY_ID \
--secret-access-key $AWS_SECRET_ACCESS_KEYcurl -X POST https://api.ittybit.com/connections \
-H "Authorization: Bearer $ITTYBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "s3",
"name": "my-uploads",
"endpoint": "https://s3.us-east-1.amazonaws.com",
"region": "us-east-1",
"access_key_id": "'$AWS_ACCESS_KEY_ID'",
"secret_access_key": "'$AWS_SECRET_ACCESS_KEY'"
}' The connection_id from the response goes into your .env as ITTYBIT_CONNECTION_ID.
Multiple outputs
Need a thumbnail alongside the transcoded video? Fire a second task after the upload.
// In VideoController::store(), after the first Http::post()
Http::withToken(config('services.ittybit.api_key'))
->post('https://api.ittybit.com/jobs', [
'input' => $s3Url,
'connection_id' => config('services.ittybit.connection_id'),
'kind' => 'image',
'options' => [
'start' => 2,
'width' => 640,
'format' => 'webp',
],
]);
Both tasks run in parallel. Handle each webhook independently by matching on ittybit_task_id.