Files
ladill-meet/app/Services/Meet/RecordingService.php
T
isaaccladandCursor b765b5dd97
Deploy Ladill Meet / deploy (push) Successful in 53s
Store meet recordings on Ladill servers via browser upload.
Capture the meeting in the host browser, upload to Ladill storage on stop, and remove the LiveKit egress placeholder job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:40:44 +00:00

142 lines
4.1 KiB
PHP

<?php
namespace App\Services\Meet;
use App\Models\Recording;
use App\Models\Session;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
class RecordingService
{
public function start(Session $session, User $host, string $layout = 'gallery'): Recording
{
$active = $session->recordings()->where('status', 'recording')->first();
if ($active) {
return $active;
}
$recording = Recording::create([
'owner_ref' => $session->owner_ref,
'session_id' => $session->id,
'status' => 'recording',
'layout' => $layout,
'started_by_ref' => $host->ownerRef(),
'started_at' => now(),
]);
AuditLogger::record(
$session->owner_ref,
'recording.started',
$session->room->organization_id,
$host->ownerRef(),
Recording::class,
$recording->id,
);
return $recording;
}
public function stop(Recording $recording, string $actorRef): Recording
{
if ($recording->status !== 'recording') {
return $recording;
}
$recording->update([
'status' => 'processing',
'ended_at' => now(),
'duration_seconds' => $recording->started_at
? (int) $recording->started_at->diffInSeconds(now())
: null,
]);
AuditLogger::record(
$recording->owner_ref,
'recording.stopped',
$recording->session->room->organization_id,
$actorRef,
Recording::class,
$recording->id,
);
return $recording->fresh();
}
/** Store a browser-captured recording on Ladill storage (not LiveKit). */
public function storeUpload(Recording $recording, UploadedFile $file): Recording
{
if ($recording->status !== 'processing') {
return $recording;
}
$session = $recording->session;
$extension = strtolower($file->getClientOriginalExtension() ?: $file->extension() ?: 'webm');
if (! in_array($extension, ['webm', 'mp4', 'mkv'], true)) {
$extension = 'webm';
}
$filename = 'recordings/'.$session->uuid.'/'.$recording->uuid.'.'.$extension;
$disk = Storage::disk(config('meet.recordings.disk', 'local'));
$disk->putFileAs(
'recordings/'.$session->uuid,
$file,
$recording->uuid.'.'.$extension,
);
$size = $disk->size($filename) ?: $file->getSize();
if ($size <= 0) {
throw new RuntimeException('Recording upload was empty.');
}
$recording->update([
'status' => 'ready',
'storage_path' => $filename,
'file_size' => $size,
'thumbnail_path' => null,
]);
AuditLogger::record(
$recording->owner_ref,
'recording.ready',
$session->room->organization_id,
null,
Recording::class,
$recording->id,
);
app(MeetNotificationService::class)->recordingReady($recording);
app(\App\Services\Integrations\WebhookDispatcher::class)->recordingReady($recording);
app(MeetBillingService::class)->chargeRecordingStorageInitial($recording->fresh());
if ($session->room->setting('auto_ai_summary', false) || config('meet.ai.auto_summarize', true)) {
app(TranscriptService::class)->ensureForSession($session, $recording);
}
return $recording->fresh();
}
public function delete(Recording $recording, string $actorRef): void
{
if ($recording->storage_path) {
Storage::disk(config('meet.recordings.disk', 'local'))->delete($recording->storage_path);
}
AuditLogger::record(
$recording->owner_ref,
'recording.deleted',
$recording->session->room->organization_id,
$actorRef,
Recording::class,
$recording->id,
);
$recording->delete();
}
}