Record meetings with LiveKit Egress instead of browser capture.
Deploy Ladill Meet / deploy (push) Successful in 52s

Server-side room composite egress replaces fragile MediaRecorder DOM capture when enabled, with webhook completion and browser fallback when egress is unavailable.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-04 03:18:28 +00:00
co-authored by Cursor
parent c2cb9cf33a
commit 4e4bed0df3
10 changed files with 674 additions and 10 deletions
@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Api;
use Agence104\LiveKit\WebhookReceiver;
use App\Http\Controllers\Controller;
use App\Models\Recording;
use App\Services\Meet\RecordingService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LiveKitWebhookController extends Controller
{
public function __invoke(Request $request, RecordingService $recordings): JsonResponse
{
try {
$receiver = new WebhookReceiver(
(string) config('meet.media.livekit.api_key'),
(string) config('meet.media.livekit.api_secret'),
);
$event = $receiver->receive(
$request->getContent(),
$request->header('Authorization'),
);
} catch (\Throwable $e) {
Log::warning('LiveKit webhook rejected.', ['error' => $e->getMessage()]);
return response()->json(['error' => 'invalid webhook'], 401);
}
if ($event->getEvent() !== 'egress_ended' || ! $event->hasEgressInfo()) {
return response()->json(['status' => 'ignored']);
}
$info = $event->getEgressInfo();
$egressId = $info->getEgressId();
if ($egressId === '') {
return response()->json(['status' => 'ignored']);
}
$recording = Recording::query()
->whereIn('status', ['recording', 'processing'])
->where('metadata->egress_id', $egressId)
->first();
if (! $recording) {
return response()->json(['status' => 'not_found']);
}
$recordings->completeFromEgress($recording, $info);
return response()->json(['status' => 'accepted']);
}
}
@@ -9,6 +9,7 @@ use App\Models\Room;
use App\Models\Session;
use App\Services\Meet\BreakoutService;
use App\Services\Meet\ChatService;
use App\Services\Meet\Media\LiveKitEgressService;
use App\Services\Meet\Media\MediaProviderInterface;
use App\Services\Meet\ParticipantPresenter;
use App\Services\Meet\RecordingService;
@@ -94,6 +95,7 @@ class MeetingRoomController extends Controller
'mediaConfigured' => $this->media->isConfigured(),
'messages' => $this->chat->recentMessages($session),
'activeRecording' => $session->recordings()->where('status', 'recording')->first(),
'serverEgressAvailable' => app(LiveKitEgressService::class)->isAvailable(),
'watermark' => $room->organization?->securitySetting('watermark_enabled', false),
'isWebinar' => $room->isWebinar() || $room->isConference(),
'isConference' => $room->isConference(),
@@ -514,7 +516,11 @@ class MeetingRoomController extends Controller
$recording = $this->recordings->start($session, $host);
return response()->json(['recording_uuid' => $recording->uuid, 'status' => $recording->status]);
return response()->json([
'recording_uuid' => $recording->uuid,
'status' => $recording->status,
'capture_mode' => $this->recordings->captureMode($recording),
]);
}
public function stopRecording(Request $request, Session $session): JsonResponse
@@ -542,6 +548,7 @@ class MeetingRoomController extends Controller
return response()->json([
'status' => 'processing',
'recording_uuid' => $recording->uuid,
'capture_mode' => $this->recordings->captureMode($recording),
]);
}