Record meetings with LiveKit Egress instead of browser capture.
Deploy Ladill Meet / deploy (push) Successful in 52s
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:
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user