activeSession(); if ($existing) { return $existing; } app(LicenseService::class)->assertCanStartSession($room->organization, $room->branch_id, $room); $session = Session::create([ 'owner_ref' => $room->owner_ref, 'room_id' => $room->id, 'media_room_name' => $this->rooms->mediaRoomName($room), 'status' => 'live', 'started_at' => now(), ]); $room->update(['status' => 'live']); $this->media->createRoom($session->media_room_name); $this->joinParticipant($session, $host, 'host'); if ($room->setting('auto_record', false)) { app(RecordingService::class)->start($session, $host); } app(MeetNotificationService::class)->meetingStarted($session, $host); app(\App\Services\Integrations\WebhookDispatcher::class)->meetingStarted($session); AuditLogger::record($room->owner_ref, 'session.started', $room->organization_id, $host->ownerRef(), Session::class, $session->id); return $session; } public function end(Session $session, string $actorRef): Session { $session->update([ 'status' => 'ended', 'ended_at' => now(), ]); $session->room->update(['status' => 'ended']); $session->participants() ->whereIn('status', ['joined', 'waiting']) ->update(['status' => 'left', 'left_at' => now()]); foreach ($session->recordings()->where('status', 'recording')->get() as $recording) { app(RecordingService::class)->stop($recording, $actorRef); } if ($session->room->isRecurring()) { app(RecurringMeetingService::class)->spawnNextOccurrences($session->room, 1); } AuditLogger::record($session->owner_ref, 'session.ended', $session->room->organization_id, $actorRef, Session::class, $session->id); app(\App\Services\Integrations\WebhookDispatcher::class)->meetingEnded($session); app(UsageMeteringService::class)->recordSessionUsage($session); return $session; } public function joinParticipant( Session $session, ?User $user, string $role, ?string $displayName = null, ?string $email = null, string $status = 'joined', ): Participant { $userRef = $user?->ownerRef(); $existing = $this->findExistingParticipant($session, $userRef, $email); if ($existing) { $updates = [ 'left_at' => null, 'display_name' => $displayName ?? $existing->display_name, 'ip_address' => request()?->ip(), ]; if ($existing->status === 'waiting') { $updates['status'] = 'waiting'; } else { $updates['status'] = 'joined'; $updates['joined_at'] = $existing->joined_at ?? now(); } if (in_array($role, ['host', 'co_host'], true)) { $updates['role'] = $role; } elseif ($existing->role === 'guest' && $role !== 'guest') { $updates['role'] = $role; } $existing->update($updates); return $existing; } $joinedAt = $status === 'joined' ? now() : null; $participant = Participant::create([ 'owner_ref' => $session->owner_ref, 'session_id' => $session->id, 'user_ref' => $userRef, 'display_name' => $displayName ?? $user?->name ?? 'Guest', 'email' => $email, 'role' => $role, 'status' => $status, 'joined_at' => $joinedAt, 'ip_address' => request()?->ip(), ]); if ($status === 'joined') { $this->updatePeakParticipants($session); } if ($status === 'joined') { AuditLogger::record( $session->owner_ref, 'participant.joined', $session->room->organization_id, $userRef, Participant::class, $participant->id, ); } return $participant; } public function admitParticipant(Participant $participant): Participant { abort_unless($participant->status === 'waiting', 422); $participant->update([ 'status' => 'joined', 'joined_at' => now(), 'left_at' => null, ]); $this->updatePeakParticipants($participant->session); AuditLogger::record( $participant->owner_ref, 'participant.joined', $participant->session->room->organization_id, $participant->user_ref, Participant::class, $participant->id, ); return $participant->fresh(); } public function denyParticipant(Participant $participant): Participant { abort_unless($participant->status === 'waiting', 422); $participant->update([ 'status' => 'removed', 'left_at' => now(), ]); AuditLogger::record( $participant->owner_ref, 'participant.removed', $participant->session->room->organization_id, $participant->user_ref, Participant::class, $participant->id, ); return $participant->fresh(); } protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant { $active = fn ($q) => $q->whereIn('status', ['invited', 'waiting', 'joined']); if ($userRef) { return $session->participants() ->where('user_ref', $userRef) ->where($active) ->first(); } if ($email) { return $session->participants() ->whereNull('user_ref') ->where('email', $email) ->where($active) ->first(); } $guestUuid = (string) request()?->session()?->get("meet.participant.{$session->uuid}", ''); if ($guestUuid === '') { return null; } return $session->participants() ->where('uuid', $guestUuid) ->whereNull('user_ref') ->where($active) ->first(); } public function rememberParticipantSession(Request $request, Participant $participant, ?Session $session = null): void { $session ??= $participant->session; $request->session()->put("meet.participant.{$session->uuid}", $participant->uuid); } public function ensureJoinedParticipant( Request $request, Session $session, ?User $user, string $role, ?string $displayName = null, ?string $email = null, ): Participant { if ($user) { $existing = $this->joinedParticipantForUser($session, $user); if ($existing) { $this->rememberParticipantSession($request, $existing, $session); return $existing; } } $participant = $this->joinParticipant($session, $user, $role, $displayName, $email); $this->rememberParticipantSession($request, $participant, $session); return $participant; } public function transitionGreenRoomParticipant( Request $request, Participant $greenParticipant, Session $liveSession, ): ?Participant { $presenters = (array) $liveSession->presenter_refs; $isPresenter = in_array($greenParticipant->role, ['host', 'co_host', 'panelist'], true) || ($greenParticipant->user_ref && in_array($greenParticipant->user_ref, $presenters, true)); if (! $isPresenter) { return null; } $user = $greenParticipant->user_ref ? User::where('public_id', $greenParticipant->user_ref)->first() : null; $role = $greenParticipant->role; if ($user && $user->ownerRef() === $liveSession->room->host_user_ref) { $role = 'host'; } elseif (! in_array($role, ['host', 'co_host', 'panelist'], true)) { $role = 'panelist'; } $greenSession = $greenParticipant->session; $participant = $this->ensureJoinedParticipant( $request, $liveSession, $user, $role, $greenParticipant->display_name, $greenParticipant->email, ); $request->session()->forget("meet.participant.{$greenSession->uuid}"); return $participant; } public function joinedParticipantForUser(Session $session, User $user): ?Participant { return $session->participants() ->where('user_ref', $user->ownerRef()) ->whereIn('status', ['invited', 'waiting', 'joined']) ->first(); } public function leaveParticipant(Participant $participant): Participant { $participant->update([ 'status' => 'left', 'left_at' => now(), 'hand_raised' => false, ]); AuditLogger::record( $participant->owner_ref, 'participant.left', $participant->session->room->organization_id, $participant->user_ref, Participant::class, $participant->id, ); return $participant; } public function generateMediaToken(Participant $participant): string { $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' => $state['can_publish'], 'can_subscribe' => true, 'can_publish_data' => true, 'room_admin' => $participant->isHost(), ], ); } /** @return array{can_publish: bool, audio_only_publish: bool, speak_granted: bool, speak_requested: 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; $canPublish = $this->canParticipantPublish($participant, $inBreakout); $audioOnlyPublish = $canPublish && $participant->isAttendee() && (bool) $participant->speak_granted && ! $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' => $canPublish, 'audio_only_publish' => $audioOnlyPublish, 'speak_granted' => (bool) $participant->speak_granted, 'speak_requested' => (bool) $participant->speak_requested, '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()) { return $room->activeSession(); } return $this->start($room, $host); } public function lock(Session $session, string $actorRef): Session { $session->update(['is_locked' => true, 'locked_at' => now()]); AuditLogger::record($session->owner_ref, 'meeting.locked', $session->room->organization_id, $actorRef, Session::class, $session->id); return $session; } public function unlock(Session $session, string $actorRef): Session { $session->update(['is_locked' => false, 'locked_at' => null]); AuditLogger::record($session->owner_ref, 'meeting.unlocked', $session->room->organization_id, $actorRef, Session::class, $session->id); return $session; } protected function updatePeakParticipants(Session $session): void { $count = $session->participants()->where('status', 'joined')->count(); if ($count > $session->peak_participants) { $session->update(['peak_participants' => $count]); } } }