From ceb9ca192e9251bdf613a7501a3318b7a50426f7 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 1 Jul 2026 17:59:18 +0000 Subject: [PATCH] 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 --- .../Controllers/Meet/RecordingController.php | 19 +++++++-- app/Models/Recording.php | 7 ++++ app/Services/Meet/RecordingService.php | 32 +++++++++++++++ resources/js/meet-room.js | 11 ++++- .../views/meet/recordings/index.blade.php | 2 +- .../views/meet/recordings/show.blade.php | 6 +-- tests/Feature/MeetRecordingUploadTest.php | 40 +++++++++++++++++++ 7 files changed, 108 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Meet/RecordingController.php b/app/Http/Controllers/Meet/RecordingController.php index 402d641..16fe5e2 100644 --- a/app/Http/Controllers/Meet/RecordingController.php +++ b/app/Http/Controllers/Meet/RecordingController.php @@ -40,6 +40,7 @@ class RecordingController extends Controller $this->authorizeAbility($request, 'meetings.view'); $this->authorizeOwner($request, $recording); $recording->load(['session.room', 'session.aiSummaries', 'session.participants']); + $recording = $this->recordings->ensureStorageValid($recording); $canManage = app(\App\Services\Meet\MeetPermissions::class) ->can($this->member($request), 'meetings.manage'); @@ -51,11 +52,23 @@ class RecordingController extends Controller { $this->authorizeAbility($request, 'meetings.view'); $this->authorizeOwner($request, $recording); - abort_unless($recording->isReady() && $recording->storage_path, 404); + $recording = $this->recordings->ensureStorageValid($recording); + abort_unless($recording->hasPlayableFile(), 404, 'Recording file is not available.'); abort_unless(app(\App\Services\Meet\MeetBillingService::class)->storageIsAccessible($recording), 402, 'Recording storage billing is overdue. Top up your Ladill wallet to download this recording.'); - return Storage::disk(config('meet.recordings.disk', 'local')) - ->download($recording->storage_path, $recording->session->room->title.'.'.pathinfo($recording->storage_path, PATHINFO_EXTENSION)); + $disk = Storage::disk(config('meet.recordings.disk', 'local')); + $extension = strtolower(pathinfo($recording->storage_path, PATHINFO_EXTENSION) ?: 'webm'); + $mime = match ($extension) { + 'webm' => 'video/webm', + 'mp4' => 'video/mp4', + 'mkv' => 'video/x-matroska', + default => 'application/octet-stream', + }; + $filename = \Illuminate\Support\Str::slug($recording->session->room->title).'.'.$extension; + + return response()->streamDownload(function () use ($disk, $recording) { + echo $disk->get($recording->storage_path); + }, $filename, ['Content-Type' => $mime]); } public function destroy(Request $request, Recording $recording): RedirectResponse diff --git a/app/Models/Recording.php b/app/Models/Recording.php index 4e6ac0b..c7a80e5 100644 --- a/app/Models/Recording.php +++ b/app/Models/Recording.php @@ -55,4 +55,11 @@ class Recording extends Model { return $this->status === 'ready'; } + + public function hasPlayableFile(): bool + { + return $this->isReady() + && filled($this->storage_path) + && (int) ($this->file_size ?? 0) > 0; + } } diff --git a/app/Services/Meet/RecordingService.php b/app/Services/Meet/RecordingService.php index 3ad5e67..9a42b5d 100644 --- a/app/Services/Meet/RecordingService.php +++ b/app/Services/Meet/RecordingService.php @@ -121,6 +121,38 @@ class RecordingService 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) { diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index a8aed1a..7018d78 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -800,7 +800,12 @@ function meetRoom() { const recordingUuid = data.recording_uuid || this.activeRecordingUuid; if (blob && recordingUuid) { - await this.uploadRecording(blob, recordingUuid); + const uploaded = await this.uploadRecording(blob, recordingUuid); + if (!uploaded) { + window.alert('Recording upload failed. Open Recordings and try again, or re-record the meeting.'); + } + } else if (recordingUuid) { + window.alert('No recording was captured from this meeting. Start recording again while the meeting is live.'); } this.activeRecordingUuid = null; @@ -834,11 +839,13 @@ function meetRoom() { const form = new FormData(); form.append('recording', blob, `${recordingUuid}.webm`); - await fetch(`/room/${this.config.sessionUuid}/recordings/${recordingUuid}/upload`, { + const res = await fetch(`/room/${this.config.sessionUuid}/recordings/${recordingUuid}/upload`, { method: 'POST', headers: { 'X-CSRF-TOKEN': this.config.csrf, Accept: 'application/json' }, body: form, }); + + return res.ok; }, async toggleLock() { diff --git a/resources/views/meet/recordings/index.blade.php b/resources/views/meet/recordings/index.blade.php index ec0c47b..61f6141 100644 --- a/resources/views/meet/recordings/index.blade.php +++ b/resources/views/meet/recordings/index.blade.php @@ -15,7 +15,7 @@
View - @if ($recording->isReady()) + @if ($recording->hasPlayableFile()) Download @endif
diff --git a/resources/views/meet/recordings/show.blade.php b/resources/views/meet/recordings/show.blade.php index bf0be99..0b3d9f5 100644 --- a/resources/views/meet/recordings/show.blade.php +++ b/resources/views/meet/recordings/show.blade.php @@ -70,8 +70,8 @@ - @if ($recording->isReady()) -

Your recording is stored on Ladill and ready to download.

+ @if ($recording->hasPlayableFile()) +

Stored on Ladill as WebM — VLC, Chrome, and QuickTime can play it after download.

@elseif ($recording->status === 'processing')
- @if ($recording->isReady()) + @if ($recording->hasPlayableFile())