From 8a1eddfe48482fb20b6f49141706ce82223fe579 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:05:24 +0200 Subject: [PATCH 1/2] Use past meeting to calc. estimated meeting size and set bbb-meeting-size-hint meta param --- app/Models/Room.php | 35 ++++++++++++ .../Defaults/ServerLoadCalculationPlugin.php | 11 +++- app/Services/MeetingService.php | 5 ++ composer.json | 1 + composer.lock | 55 ++++++++++++++++++- config/bigbluebutton.php | 1 + 6 files changed, 106 insertions(+), 2 deletions(-) diff --git a/app/Models/Room.php b/app/Models/Room.php index 4c2bf03f3b..44ff2dc2b5 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -10,6 +10,7 @@ use App\Observers\RoomObserver; use App\Settings\GeneralSettings; use App\Traits\AddsModelNameTrait; +use HiFolks\Statistics\Stat; use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -430,4 +431,38 @@ public function streaming(): HasOne ] )->chaperone('room'); } + + /** + * Get the estimated max participants of the room based on the last x meetings. + */ + public function getEstimatedMaxParticipants(int $limit = 10): ?int + { + $meetings = $this->meetings() + ->whereNotNull('end') + ->orderBy('end', 'desc') + ->limit($limit) + ->pluck('id'); + + if ($meetings->isEmpty()) { + return null; + } + + $maxParticipants = MeetingStat::whereIn('meeting_id', $meetings) + ->groupBy('meeting_id') + ->selectRaw('max(participant_count) as max_participants') + ->orderBy('created_at') + ->pluck('max_participants'); + + if ($maxParticipants->isEmpty()) { + return null; + } + + // Calculate weighted median + // Using median over average to compensate outliners + // Use a weighted median to give more weight to newer meetings, as they are more relevant for the future than older meetings + $weights = range(1, $maxParticipants->count()); + $weightedMedian = Stat::weightedMedian($maxParticipants->toArray(), $weights); + + return (int) ceil($weightedMedian); + } } diff --git a/app/Plugins/Defaults/ServerLoadCalculationPlugin.php b/app/Plugins/Defaults/ServerLoadCalculationPlugin.php index 07d28dff98..c64f7f5396 100644 --- a/app/Plugins/Defaults/ServerLoadCalculationPlugin.php +++ b/app/Plugins/Defaults/ServerLoadCalculationPlugin.php @@ -5,10 +5,14 @@ namespace App\Plugins\Defaults; use App\Plugins\Contracts\ServerLoadCalculationPluginContract; +use BigBlueButton\Core\Meeting; use Carbon\Carbon; class ServerLoadCalculationPlugin implements ServerLoadCalculationPluginContract { + /** + * @param Meeting[] $meetings + */ public function getLoad(array $meetings): int { $load = 0; @@ -24,7 +28,12 @@ public function getLoad(array $meetings): int // If meeting is in the starting phase, we use a higher number of users to calculate // the load to compensate that the meeting will probably have more users in the future - $minUserCount = config('bigbluebutton.load_new_meeting_min_user_count'); + + // Use bbb-meeting-size-hint metadata if provided, fallback to fixed configurable value. + // (bbb-meeting-size-hint is set by PILOS based on previous meetings, other frontends e.g. an LMSs might set it to the class size, etc.) + $metadata = $meeting->getMetas(); + $meetingSizeHint = isset($metadata['bbb-meeting-size-hint']) && is_numeric($metadata['bbb-meeting-size-hint']) ? (int) $metadata['bbb-meeting-size-hint'] : null; + $minUserCount = $meetingSizeHint ?? config('bigbluebutton.load_new_meeting_min_user_count'); // However if the meeting has a max user limit the meeting will never go over that limit, // so we use the max user limit to calculate the load if it is lower than the min user count diff --git a/app/Services/MeetingService.php b/app/Services/MeetingService.php index 82cc47efcd..08c5e2b50c 100644 --- a/app/Services/MeetingService.php +++ b/app/Services/MeetingService.php @@ -107,6 +107,11 @@ public function start(): ?CreateMeetingResponse $meetingParams->addMeta('bbb-origin', 'PILOS'); $meetingParams->addMeta('pilos-sub-spool-dir', config('recording.spool-sub-directory')); + $estimatedMax = $this->meeting->room->getEstimatedMaxParticipants(config('bigbluebutton.meeting_size_hint_meetings_count')); + if ($estimatedMax !== null) { + $meetingParams->addMeta('bbb-meeting-size-hint', (string) $estimatedMax); + } + // get files that should be used in this meeting and add links to the files $files = $this->meeting->room->files()->where('use_in_meeting', true)->orderBy('default', 'desc')->get(); foreach ($files as $file) { diff --git a/composer.json b/composer.json index 54fc51a8fb..fd158e022d 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "ext-zip": "*", "directorytree/ldaprecord-laravel": "^4.0", "guzzlehttp/guzzle": "^7.0.1", + "hi-folks/statistics": "^1.5", "laravel/fortify": "^1.36", "laravel/framework": "^13.2", "laravel/horizon": "^5.21", diff --git a/composer.lock b/composer.lock index 6f834fc636..a3a61fbd31 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f311dccfca0ec0a65ebaf789564c8cb", + "content-hash": "6e73e8207d6669d915e9a870e67096d8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1559,6 +1559,59 @@ ], "time": "2026-05-23T22:00:21+00:00" }, + { + "name": "hi-folks/statistics", + "version": "v1.5.2", + "source": { + "type": "git", + "url": "https://github.com/Hi-Folks/statistics.git", + "reference": "5d1b723c57ca4a8f23ae2b8966f77dea26b59548" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Hi-Folks/statistics/zipball/5d1b723c57ca4a8f23ae2b8966f77dea26b59548", + "reference": "5d1b723c57ca4a8f23ae2b8966f77dea26b59548", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|8.5" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.65", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^11.0", + "rector/rector": "^2" + }, + "type": "library", + "autoload": { + "psr-4": { + "HiFolks\\Statistics\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roberto B.", + "email": "roberto.butti@gmail.com", + "role": "Developer" + } + ], + "description": "PHP package that provides functions for calculating mathematical statistics of numeric data.", + "homepage": "https://github.com/hi-folks/statistics", + "keywords": [ + "hi-folks", + "statistics" + ], + "support": { + "issues": "https://github.com/Hi-Folks/statistics/issues", + "source": "https://github.com/Hi-Folks/statistics/tree/v1.5.2" + }, + "time": "2026-05-13T06:01:10+00:00" + }, { "name": "laravel/fortify", "version": "v1.37.2", diff --git a/config/bigbluebutton.php b/config/bigbluebutton.php index e51b67dd83..a25c895755 100644 --- a/config/bigbluebutton.php +++ b/config/bigbluebutton.php @@ -23,6 +23,7 @@ 'load_new_meeting_min_user_count' => (int) env('BBB_LOAD_MIN_USER_COUNT', 15), 'load_new_meeting_min_user_interval' => (int) env('BBB_LOAD_MIN_USER_INTERVAL', 15), + 'meeting_size_hint_meetings_count' => (int) env('BBB_MEETING_SIZE_HINT_MEETINGS_COUNT', 10), 'allowed_name_characters' => env('BBB_ALLOWED_NAME_CHARACTERS', "\w ,.'\-+\/&()"), From 46af0691a5b1d0aa881928bec7d17fbbc2bcc775 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:11:14 +0200 Subject: [PATCH 2/2] Ignore short meetings --- app/Models/Room.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/Models/Room.php b/app/Models/Room.php index 44ff2dc2b5..0ce7d2265f 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Support\Facades\Context; +use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; #[ObservedBy([RoomObserver::class])] @@ -439,9 +440,25 @@ public function getEstimatedMaxParticipants(int $limit = 10): ?int { $meetings = $this->meetings() ->whereNotNull('end') - ->orderBy('end', 'desc') - ->limit($limit) - ->pluck('id'); + ->whereNotNull('start') + ->limit($limit); + + $db = DB::connection()->getConfig(); + + // Get only meetings that lasted at least 10 min. + switch ($db['driver']) { + case 'mariadb': + case 'mysql': + $meetings->whereRaw('end > start + INTERVAL 10 MINUTE'); + break; + case 'pgsql': + $meetings->whereRaw("end - start > INTERVAL '10 minutes'"); + break; + default: + throw new \Exception('Database driver not supported'); + } + + $meetings = $meetings->pluck('id'); if ($meetings->isEmpty()) { return null;