From d2bf08ec452b04bb1b3dd240c90414747c930f89 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 4 Jul 2026 03:45:49 +0000 Subject: [PATCH] Add Contabo Object Storage support for Meet egress recordings. Use a dedicated meet-recordings S3 disk with path-style endpoints for Contabo, and pass force_path_style through to LiveKit egress uploads. Co-authored-by: Cursor --- .env.example | 10 ++++++--- .../Meet/Media/LiveKitEgressService.php | 5 +++-- config/filesystems.php | 17 ++++++++++++++ config/meet.php | 6 ++++- .../MeetLiveKitEgressRecordingTest.php | 22 +++++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index d46401d..2a83b55 100644 --- a/.env.example +++ b/.env.example @@ -45,16 +45,20 @@ LIVEKIT_API_KEY= LIVEKIT_API_SECRET= # --- Recordings & AI --- -MEET_RECORDINGS_DISK=local +# Use meet-recordings disk when LiveKit egress writes to Contabo Object Storage. +MEET_RECORDINGS_DISK=meet-recordings MEET_RECORDING_MAX_MB=512 # Server-side recording via LiveKit Egress (recommended for production) MEET_EGRESS_ENABLED=false MEET_EGRESS_WEBHOOK_URL= +# Contabo Object Storage (S3-compatible). Keys: Object Storage panel → Account → Security & Access. +# Endpoint examples: https://eu2.contabostorage.com https://sin1.contabostorage.com MEET_EGRESS_S3_BUCKET= -MEET_EGRESS_S3_REGION= +MEET_EGRESS_S3_REGION=default +MEET_EGRESS_S3_ENDPOINT=https://eu2.contabostorage.com +MEET_EGRESS_S3_USE_PATH_STYLE=true MEET_EGRESS_S3_ACCESS_KEY= MEET_EGRESS_S3_SECRET= -MEET_EGRESS_S3_ENDPOINT= MEET_AI_DRIVER=openai MEET_AI_API_KEY= MEET_AI_MODEL=gpt-4o-mini diff --git a/app/Services/Meet/Media/LiveKitEgressService.php b/app/Services/Meet/Media/LiveKitEgressService.php index 2c9ac03..94d6141 100644 --- a/app/Services/Meet/Media/LiveKitEgressService.php +++ b/app/Services/Meet/Media/LiveKitEgressService.php @@ -90,7 +90,7 @@ class LiveKitEgressService } /** - * @return array|null + * @return array|null */ protected function s3UploadConfig(): ?array { @@ -103,7 +103,8 @@ class LiveKitEgressService $config = [ 'bucket' => $bucket, - 'region' => (string) ($egress['region'] ?? 'us-east-1'), + 'region' => (string) ($egress['region'] ?? 'default'), + 'force_path_style' => (bool) ($egress['force_path_style'] ?? true), ]; foreach (['access_key', 'secret', 'session_token', 'endpoint', 'assume_role_arn', 'assume_role_external_id'] as $key) { diff --git a/config/filesystems.php b/config/filesystems.php index 4ebaa0b..1ca109e 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -60,6 +60,23 @@ return [ 'report' => false, ], + // Contabo (or other S3-compatible) bucket for Meet recordings — same creds as LiveKit egress. + 'meet-recordings' => [ + 'driver' => 's3', + 'key' => env('MEET_EGRESS_S3_ACCESS_KEY', env('AWS_ACCESS_KEY_ID')), + 'secret' => env('MEET_EGRESS_S3_SECRET', env('AWS_SECRET_ACCESS_KEY')), + 'region' => env('MEET_EGRESS_S3_REGION', env('AWS_DEFAULT_REGION', 'default')), + 'bucket' => env('MEET_EGRESS_S3_BUCKET', env('AWS_BUCKET')), + 'url' => env('MEET_EGRESS_S3_URL', env('AWS_URL')), + 'endpoint' => env('MEET_EGRESS_S3_ENDPOINT', env('AWS_ENDPOINT')), + 'use_path_style_endpoint' => filter_var( + env('MEET_EGRESS_S3_USE_PATH_STYLE', env('AWS_USE_PATH_STYLE_ENDPOINT', true)), + FILTER_VALIDATE_BOOL, + ), + 'throw' => false, + 'report' => false, + ], + 'qr' => [ 'driver' => 'local', 'root' => storage_path('app/private/qr'), diff --git a/config/meet.php b/config/meet.php index 4488f86..97f50b9 100644 --- a/config/meet.php +++ b/config/meet.php @@ -121,9 +121,13 @@ return [ 'access_key' => env('MEET_EGRESS_S3_ACCESS_KEY', env('AWS_ACCESS_KEY_ID')), 'secret' => env('MEET_EGRESS_S3_SECRET', env('AWS_SECRET_ACCESS_KEY')), 'session_token' => env('MEET_EGRESS_S3_SESSION_TOKEN'), - 'region' => env('MEET_EGRESS_S3_REGION', env('AWS_DEFAULT_REGION', 'us-east-1')), + 'region' => env('MEET_EGRESS_S3_REGION', env('AWS_DEFAULT_REGION', 'default')), 'bucket' => env('MEET_EGRESS_S3_BUCKET', env('AWS_BUCKET')), 'endpoint' => env('MEET_EGRESS_S3_ENDPOINT', env('AWS_ENDPOINT')), + 'force_path_style' => filter_var( + env('MEET_EGRESS_S3_USE_PATH_STYLE', env('AWS_USE_PATH_STYLE_ENDPOINT', true)), + FILTER_VALIDATE_BOOL, + ), 'assume_role_arn' => env('MEET_EGRESS_S3_ASSUME_ROLE_ARN'), 'assume_role_external_id' => env('MEET_EGRESS_S3_ASSUME_ROLE_EXTERNAL_ID'), ], diff --git a/tests/Feature/MeetLiveKitEgressRecordingTest.php b/tests/Feature/MeetLiveKitEgressRecordingTest.php index 88dac56..487cf7f 100644 --- a/tests/Feature/MeetLiveKitEgressRecordingTest.php +++ b/tests/Feature/MeetLiveKitEgressRecordingTest.php @@ -203,6 +203,28 @@ class MeetLiveKitEgressRecordingTest extends TestCase $this->assertSame('encoder crashed', $recording->failure_reason); } + public function test_s3_upload_config_uses_contabo_path_style_defaults(): void + { + config([ + 'meet.recordings.egress.s3.bucket' => 'ladill-meet', + 'meet.recordings.egress.s3.endpoint' => 'https://eu2.contabostorage.com', + 'meet.recordings.egress.s3.region' => 'default', + 'meet.recordings.egress.s3.force_path_style' => true, + 'meet.recordings.egress.s3.access_key' => 'access', + 'meet.recordings.egress.s3.secret' => 'secret', + ]); + + $service = app(LiveKitEgressService::class); + $method = new \ReflectionMethod($service, 's3UploadConfig'); + $method->setAccessible(true); + $config = $method->invoke($service); + + $this->assertSame('ladill-meet', $config['bucket']); + $this->assertSame('https://eu2.contabostorage.com', $config['endpoint']); + $this->assertSame('default', $config['region']); + $this->assertTrue($config['force_path_style']); + } + /** * @return array{0: Session, 1: Participant} */