Implement conference breakouts with meeting mode and programme lineup UI.
Deploy Ladill Meet / deploy (push) Successful in 1m3s
Deploy Ladill Meet / deploy (push) Successful in 1m3s
Attendees reconnect to breakout rooms with publish access while hosts stay on stage; poll and media-token endpoints drive LiveKit reconnection, and the live room shows Events-linked programme when available. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Models\Participant;
|
||||
use App\Models\Recording;
|
||||
use App\Models\Room;
|
||||
use App\Models\Session;
|
||||
use App\Services\Meet\BreakoutService;
|
||||
use App\Services\Meet\ChatService;
|
||||
use App\Services\Meet\Media\MediaProviderInterface;
|
||||
use App\Services\Meet\ParticipantPresenter;
|
||||
@@ -69,6 +70,8 @@ class MeetingRoomController extends Controller
|
||||
$authUser = $request->user();
|
||||
$isRoomHost = $authUser && $authUser->ownerRef() === $room->host_user_ref;
|
||||
$canManageConference = $participant->isHost() || $isRoomHost;
|
||||
$breakoutService = app(BreakoutService::class);
|
||||
$inBreakout = $breakoutService->isInActiveBreakout($participant);
|
||||
|
||||
return view('meet.room.show', [
|
||||
'session' => $session,
|
||||
@@ -87,13 +90,14 @@ class MeetingRoomController extends Controller
|
||||
'presenterRefs' => (array) $session->presenter_refs,
|
||||
'isAudioOnly' => $isAudioOnly,
|
||||
'isSpace' => $room->isSpace(),
|
||||
'canPublish' => $room->isSpace()
|
||||
? $spaces->canPublish($room, $participant)
|
||||
: $webinars->canPublish($room, $participant),
|
||||
'canPublish' => $this->sessions->canParticipantPublish($participant),
|
||||
'isAttendee' => $participant->isAttendee() || $spaces->isListener($room, $participant),
|
||||
'usesStageLayout' => $stageLayout->usesStageLayout($room),
|
||||
'usesStageLayout' => $stageLayout->usesStageLayout($room) && ! $inBreakout,
|
||||
'inBreakout' => $inBreakout,
|
||||
'stageConfig' => $stageLayout->config($room),
|
||||
'canManageConference' => $canManageConference,
|
||||
'programmeSnapshot' => $room->setting('programme_snapshot'),
|
||||
'linkedProgrammeItems' => (array) $room->setting('linked_programme_items', []),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -229,7 +233,11 @@ class MeetingRoomController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$this->currentParticipant($request, $session);
|
||||
$participant = $this->currentParticipant($request, $session);
|
||||
|
||||
app(BreakoutService::class)->closeExpired($session);
|
||||
|
||||
$participant->refresh();
|
||||
|
||||
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
|
||||
|
||||
@@ -295,9 +303,24 @@ class MeetingRoomController extends Controller
|
||||
'original_name' => $f->original_name,
|
||||
'file_size' => $f->file_size,
|
||||
]),
|
||||
'media' => $this->sessions->mediaSessionState($participant),
|
||||
'breakouts_active' => app(BreakoutService::class)->hasActiveBreakouts($session),
|
||||
], app(StageLayoutService::class)->config($session->room)));
|
||||
}
|
||||
|
||||
public function mediaToken(Request $request, Session $session): JsonResponse
|
||||
{
|
||||
abort_unless($session->isLive(), 404);
|
||||
|
||||
$participant = $this->currentParticipant($request, $session);
|
||||
|
||||
return response()->json(array_merge(
|
||||
['token' => $this->sessions->generateMediaToken($participant)],
|
||||
$this->sessions->mediaSessionState($participant->fresh()),
|
||||
['breakouts_active' => app(BreakoutService::class)->hasActiveBreakouts($session)],
|
||||
));
|
||||
}
|
||||
|
||||
public function startRecording(Request $request, Session $session): JsonResponse
|
||||
{
|
||||
$participant = $this->currentParticipant($request, $session);
|
||||
|
||||
@@ -40,7 +40,10 @@ class BreakoutService
|
||||
]));
|
||||
}
|
||||
|
||||
$participants = $session->participants()->where('status', 'joined')->get();
|
||||
$participants = $session->participants()
|
||||
->where('status', 'joined')
|
||||
->whereNotIn('role', ['host', 'co_host'])
|
||||
->get();
|
||||
|
||||
if ($assignments) {
|
||||
foreach ($assignments as $assignment) {
|
||||
@@ -95,6 +98,34 @@ class BreakoutService
|
||||
BreakoutAssignment::whereIn('breakout_room_id', $session->breakoutRooms()->pluck('id'))->delete();
|
||||
}
|
||||
|
||||
public function hasActiveBreakouts(Session $session): bool
|
||||
{
|
||||
return $session->breakoutRooms()->where('status', 'open')->exists();
|
||||
}
|
||||
|
||||
public function closeExpired(Session $session): bool
|
||||
{
|
||||
if (! $session->breakoutRooms()->where('status', 'open')->where('closes_at', '<=', now())->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->closeAll($session);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isInActiveBreakout(Participant $participant): bool
|
||||
{
|
||||
if (! $participant->breakout_room_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return BreakoutRoom::query()
|
||||
->where('id', $participant->breakout_room_id)
|
||||
->where('status', 'open')
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function mediaRoomForParticipant(Participant $participant): string
|
||||
{
|
||||
if ($participant->breakout_room_id) {
|
||||
|
||||
@@ -263,30 +263,70 @@ class SessionService
|
||||
|
||||
public function generateMediaToken(Participant $participant): string
|
||||
{
|
||||
$session = $participant->session;
|
||||
$room = $session->room;
|
||||
$isHost = $participant->isHost();
|
||||
$breakoutService = app(BreakoutService::class);
|
||||
$webinarService = app(WebinarService::class);
|
||||
$spaceService = app(\App\Services\Meet\SpaceService::class);
|
||||
$mediaRoom = $breakoutService->mediaRoomForParticipant($participant);
|
||||
$canPublish = ($room->isSpace()
|
||||
? $spaceService->canPublish($room, $participant)
|
||||
: $webinarService->canPublish($room, $participant)) && $participant->canPublishMedia();
|
||||
$participant->loadMissing('breakoutRoom');
|
||||
$state = $this->mediaSessionState($participant);
|
||||
$mediaRoom = app(BreakoutService::class)->mediaRoomForParticipant($participant);
|
||||
|
||||
return $this->media->generateToken(
|
||||
$mediaRoom,
|
||||
$participant->uuid,
|
||||
$participant->display_name,
|
||||
[
|
||||
'can_publish' => $canPublish,
|
||||
'can_publish' => $state['can_publish'],
|
||||
'can_subscribe' => true,
|
||||
'can_publish_data' => true,
|
||||
'room_admin' => $isHost,
|
||||
'room_admin' => $participant->isHost(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{can_publish: bool, uses_stage_layout: bool, in_breakout: bool, breakout_room_id: ?int, breakout: ?array{uuid: string, name: string, closes_at: ?string}} */
|
||||
public function mediaSessionState(Participant $participant): array
|
||||
{
|
||||
$participant->loadMissing(['breakoutRoom', 'session.room']);
|
||||
$room = $participant->session->room;
|
||||
$breakoutService = app(BreakoutService::class);
|
||||
$inBreakout = $breakoutService->isInActiveBreakout($participant);
|
||||
$usesStageLayout = app(StageLayoutService::class)->usesStageLayout($room) && ! $inBreakout;
|
||||
|
||||
$breakout = null;
|
||||
if ($inBreakout && $participant->breakoutRoom) {
|
||||
$breakout = [
|
||||
'uuid' => $participant->breakoutRoom->uuid,
|
||||
'name' => $participant->breakoutRoom->name,
|
||||
'closes_at' => $participant->breakoutRoom->closes_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'can_publish' => $this->canParticipantPublish($participant, $inBreakout),
|
||||
'uses_stage_layout' => $usesStageLayout,
|
||||
'in_breakout' => $inBreakout,
|
||||
'breakout_room_id' => $inBreakout ? $participant->breakout_room_id : null,
|
||||
'breakout' => $breakout,
|
||||
];
|
||||
}
|
||||
|
||||
public function canParticipantPublish(Participant $participant, ?bool $inBreakout = null): bool
|
||||
{
|
||||
$breakoutService = app(BreakoutService::class);
|
||||
$inBreakout ??= $breakoutService->isInActiveBreakout($participant);
|
||||
|
||||
if ($inBreakout) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$room = $participant->session->room;
|
||||
|
||||
if ($room->isSpace()) {
|
||||
return app(\App\Services\Meet\SpaceService::class)->canPublish($room, $participant)
|
||||
&& $participant->canPublishMedia();
|
||||
}
|
||||
|
||||
return app(WebinarService::class)->canPublish($room, $participant)
|
||||
&& $participant->canPublishMedia();
|
||||
}
|
||||
|
||||
public function getOrStartSession(Room $room, User $host): Session
|
||||
{
|
||||
if ($room->activeSession()) {
|
||||
|
||||
Reference in New Issue
Block a user