Files
ladill-meet/app/Services/Meet/RecordingService.php
T
isaaccladandCursor ceb9ca192e
Deploy Ladill Meet / deploy (push) Successful in 31s
Reject empty recording downloads and store WebM with correct MIME.
Mark legacy zero-byte placeholders as failed, validate files on show/download, and surface upload errors in the meeting room.

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

174 lines
5.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();
}
/** Mark ready recordings with missing/empty files as failed. */
public function ensureStorageValid(Recording $recording): Recording
{
if ($recording->status !== 'ready' || ! $recording->storage_path) {
return $recording;
}
$disk = Storage::disk(config('meet.recordings.disk', 'local'));
$size = $disk->exists($recording->storage_path) ? (int) $disk->size($recording->storage_path) : 0;
if ($size > 0) {
if ((int) ($recording->file_size ?? 0) !== $size) {
$recording->update(['file_size' => $size]);
}
return $recording->fresh();
}
if ($recording->storage_path) {
$disk->delete($recording->storage_path);
}
$recording->update([
'status' => 'failed',
'storage_path' => null,
'file_size' => null,
'failure_reason' => 'Recording file is missing or empty. Please record the meeting again.',
]);
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();
}
}