Add Contabo Object Storage support for Meet egress recordings.
Deploy Ladill Meet / deploy (push) Successful in 49s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-04 03:45:49 +00:00
co-authored by Cursor
parent ebc11691e2
commit d2bf08ec45
5 changed files with 54 additions and 6 deletions
+7 -3
View File
@@ -45,16 +45,20 @@ LIVEKIT_API_KEY=
LIVEKIT_API_SECRET= LIVEKIT_API_SECRET=
# --- Recordings & AI --- # --- 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 MEET_RECORDING_MAX_MB=512
# Server-side recording via LiveKit Egress (recommended for production) # Server-side recording via LiveKit Egress (recommended for production)
MEET_EGRESS_ENABLED=false MEET_EGRESS_ENABLED=false
MEET_EGRESS_WEBHOOK_URL= 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_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_ACCESS_KEY=
MEET_EGRESS_S3_SECRET= MEET_EGRESS_S3_SECRET=
MEET_EGRESS_S3_ENDPOINT=
MEET_AI_DRIVER=openai MEET_AI_DRIVER=openai
MEET_AI_API_KEY= MEET_AI_API_KEY=
MEET_AI_MODEL=gpt-4o-mini MEET_AI_MODEL=gpt-4o-mini
@@ -90,7 +90,7 @@ class LiveKitEgressService
} }
/** /**
* @return array<string, string>|null * @return array<string, bool|string>|null
*/ */
protected function s3UploadConfig(): ?array protected function s3UploadConfig(): ?array
{ {
@@ -103,7 +103,8 @@ class LiveKitEgressService
$config = [ $config = [
'bucket' => $bucket, '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) { foreach (['access_key', 'secret', 'session_token', 'endpoint', 'assume_role_arn', 'assume_role_external_id'] as $key) {
+17
View File
@@ -60,6 +60,23 @@ return [
'report' => false, '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' => [ 'qr' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app/private/qr'), 'root' => storage_path('app/private/qr'),
+5 -1
View File
@@ -121,9 +121,13 @@ return [
'access_key' => env('MEET_EGRESS_S3_ACCESS_KEY', env('AWS_ACCESS_KEY_ID')), 'access_key' => env('MEET_EGRESS_S3_ACCESS_KEY', env('AWS_ACCESS_KEY_ID')),
'secret' => env('MEET_EGRESS_S3_SECRET', env('AWS_SECRET_ACCESS_KEY')), 'secret' => env('MEET_EGRESS_S3_SECRET', env('AWS_SECRET_ACCESS_KEY')),
'session_token' => env('MEET_EGRESS_S3_SESSION_TOKEN'), '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')), 'bucket' => env('MEET_EGRESS_S3_BUCKET', env('AWS_BUCKET')),
'endpoint' => env('MEET_EGRESS_S3_ENDPOINT', env('AWS_ENDPOINT')), '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_arn' => env('MEET_EGRESS_S3_ASSUME_ROLE_ARN'),
'assume_role_external_id' => env('MEET_EGRESS_S3_ASSUME_ROLE_EXTERNAL_ID'), 'assume_role_external_id' => env('MEET_EGRESS_S3_ASSUME_ROLE_EXTERNAL_ID'),
], ],
@@ -203,6 +203,28 @@ class MeetLiveKitEgressRecordingTest extends TestCase
$this->assertSame('encoder crashed', $recording->failure_reason); $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} * @return array{0: Session, 1: Participant}
*/ */