From c75f9122d43903df8665eb53094ca0ef40239636 Mon Sep 17 00:00:00 2001 From: Blax Software Date: Sat, 11 Jul 2026 10:53:54 +0200 Subject: [PATCH] feat: backend-defined rooms with per-room transport (RoomRegistry + RoomMode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rooms become entities the BACKEND defines — clients can only join a registered room, and each room configures how its media is carried: - Rooms\RoomMode: 'p2p' (browsers mesh; server relays signaling only) vs 'sfu' (server terminates media via the MediaEngine/Rust SFU). - Rooms\RoomDefinition: id + mode + limit + RoomType (explicit or from the id prefix) + free-form meta for the host UI. - Contracts\RoomRegistry + Rooms\ConfigRoomRegistry (reads config webrtc.rooms; supports shorthand 'room' => 'p2p'). The provider binding is host-overridable via config webrtc.room_registry so DYNAMIC (DB/Eloquent) registries plug in — that is how learn-atc sources rooms from its talk_rooms table at runtime. 5 new tests (66 total green). Card #1087 (rel #1083 #1085). Co-Authored-By: Claude Fable 5 --- config/webrtc.php | 22 +++++++++ src/Contracts/RoomRegistry.php | 28 ++++++++++++ src/Rooms/ConfigRoomRegistry.php | 54 ++++++++++++++++++++++ src/Rooms/RoomDefinition.php | 60 ++++++++++++++++++++++++ src/Rooms/RoomMode.php | 34 ++++++++++++++ src/WebRtcServiceProvider.php | 14 ++++++ tests/RoomRegistryTest.php | 78 ++++++++++++++++++++++++++++++++ 7 files changed, 290 insertions(+) create mode 100644 src/Contracts/RoomRegistry.php create mode 100644 src/Rooms/ConfigRoomRegistry.php create mode 100644 src/Rooms/RoomDefinition.php create mode 100644 src/Rooms/RoomMode.php create mode 100644 tests/RoomRegistryTest.php diff --git a/config/webrtc.php b/config/webrtc.php index 03de6c9..8b4a09e 100644 --- a/config/webrtc.php +++ b/config/webrtc.php @@ -91,6 +91,28 @@ return [ 'format' => env('WEBRTC_RECORDING_FORMAT', 'webm'), ], + /* + |-------------------------------------------------------------------------- + | Backend-defined rooms (the room registry) + |-------------------------------------------------------------------------- + | The rooms clients are allowed to join. A join to anything NOT listed here is + | rejected — clients can't invent rooms. Each room configures its transport: + | 'p2p' — browsers mesh, the server only relays signaling (no server media) + | 'sfu' — the server terminates media (the Rust SFU; enables recording/scale) + | + | Shapes (all equivalent granularity): + | 'just-talk' => 'p2p', + | 'lobby' => ['mode' => 'sfu', 'limit' => 50, 'type' => 'presence'], + | 'private-desk' => ['mode' => 'p2p'], // membership type inferred from prefix + | + | For DYNAMIC rooms (created/edited at runtime), point `room_registry` at your + | own Contracts\RoomRegistry (e.g. an Eloquent-backed one); the static `rooms` + | map below is the fallback used when no registry class is bound. + */ + 'room_registry' => env('WEBRTC_ROOM_REGISTRY'), + + 'rooms' => [], + /* |-------------------------------------------------------------------------- | Room authorization + call-event log diff --git a/src/Contracts/RoomRegistry.php b/src/Contracts/RoomRegistry.php new file mode 100644 index 0000000..7fef2cd --- /dev/null +++ b/src/Contracts/RoomRegistry.php @@ -0,0 +1,28 @@ + keyed by room id */ + public function all(): array; +} diff --git a/src/Rooms/ConfigRoomRegistry.php b/src/Rooms/ConfigRoomRegistry.php new file mode 100644 index 0000000..4bab86c --- /dev/null +++ b/src/Rooms/ConfigRoomRegistry.php @@ -0,0 +1,54 @@ + ['mode' => 'sfu', 'limit' => 50, 'type' => 'presence'], + * 'just-talk' => ['mode' => 'p2p', 'limit' => 20], + * 'private-desk' => ['mode' => 'p2p'], // type inferred from the id prefix + * + * A bare value is also accepted as shorthand for the mode: `'just-talk' => 'p2p'`. + */ +final class ConfigRoomRegistry implements RoomRegistry +{ + /** @var array */ + private array $rooms; + + /** @param array $rooms */ + public function __construct(array $rooms) + { + $this->rooms = []; + foreach ($rooms as $id => $config) { + $id = (string) $id; + $this->rooms[$id] = is_array($config) + ? RoomDefinition::fromConfig($id, $config) + : new RoomDefinition($id, RoomMode::fromString(is_string($config) ? $config : null)); + } + } + + public function has(string $roomId): bool + { + return isset($this->rooms[$roomId]); + } + + public function get(string $roomId): ?RoomDefinition + { + return $this->rooms[$roomId] ?? null; + } + + public function mode(string $roomId): ?RoomMode + { + return $this->rooms[$roomId]?->mode; + } + + public function all(): array + { + return $this->rooms; + } +} diff --git a/src/Rooms/RoomDefinition.php b/src/Rooms/RoomDefinition.php new file mode 100644 index 0000000..db7e15e --- /dev/null +++ b/src/Rooms/RoomDefinition.php @@ -0,0 +1,60 @@ + $meta free-form (label, description, …) for the app/UI */ + public function __construct( + public readonly string $id, + public readonly RoomMode $mode = RoomMode::P2P, + public readonly ?int $limit = null, + public readonly ?RoomType $type = null, + public readonly array $meta = [], + ) {} + + public function roomType(): RoomType + { + return $this->type ?? RoomType::fromName($this->id); + } + + /** + * Build from a declarative array (the config shape): + * 'lobby' => ['mode' => 'sfu', 'limit' => 50, 'type' => 'presence', 'label' => '…'] + * + * @param array $config + */ + public static function fromConfig(string $id, array $config): self + { + $known = ['mode', 'limit', 'type']; + $meta = array_diff_key($config, array_flip($known)); + + return new self( + id: $id, + mode: RoomMode::fromString($config['mode'] ?? null), + limit: isset($config['limit']) ? (int) $config['limit'] : null, + type: isset($config['type']) ? RoomType::tryFrom((string) $config['type']) : null, + meta: $meta, + ); + } + + /** @return array */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'mode' => $this->mode->value, + 'limit' => $this->limit, + 'type' => $this->roomType()->value, + 'meta' => $this->meta, + ]; + } +} diff --git a/src/Rooms/RoomMode.php b/src/Rooms/RoomMode.php new file mode 100644 index 0000000..b6980dd --- /dev/null +++ b/src/Rooms/RoomMode.php @@ -0,0 +1,34 @@ +make(RoomStore::class)); }); + // The backend-defined room registry: which rooms exist and each room's + // transport (p2p vs server-handled). Defaults to the config-driven registry; + // a host binds its own (e.g. a DB/Eloquent-backed registry so rooms are + // created + edited at runtime) via `webrtc.room_registry`. + $this->app->bind(RoomRegistry::class, function ($app) { + $class = config('webrtc.room_registry'); + + return $class + ? $app->make($class) + : new ConfigRoomRegistry((array) config('webrtc.rooms', [])); + }); + // Where a call's audio is stored (per participant). Host may override the class. $this->app->bind(RecordingStore::class, function () { $recording = (array) config('webrtc.recording', []); diff --git a/tests/RoomRegistryTest.php b/tests/RoomRegistryTest.php new file mode 100644 index 0000000..68cf2b8 --- /dev/null +++ b/tests/RoomRegistryTest.php @@ -0,0 +1,78 @@ + 'p2p', // shorthand + 'lobby' => ['mode' => 'sfu', 'limit' => 50, 'type' => 'presence'], + 'private-desk' => ['mode' => 'p2p'], // type from prefix + 'default-mode' => [], // defaults to p2p + ]); + } + + public function test_only_registered_rooms_exist(): void + { + $reg = $this->registry(); + + $this->assertTrue($reg->has('just-talk')); + $this->assertTrue($reg->has('lobby')); + $this->assertFalse($reg->has('anything-else')); + $this->assertNull($reg->get('anything-else')); + $this->assertNull($reg->mode('anything-else')); + } + + public function test_mode_is_configured_per_room(): void + { + $reg = $this->registry(); + + $this->assertSame(RoomMode::P2P, $reg->mode('just-talk')); + $this->assertSame(RoomMode::Sfu, $reg->mode('lobby')); + $this->assertTrue($reg->get('lobby')->mode->isServerHandled()); + $this->assertFalse($reg->get('just-talk')->mode->isServerHandled()); + $this->assertSame(RoomMode::P2P, $reg->mode('default-mode'), 'missing mode defaults to p2p'); + } + + public function test_limit_and_type_resolution(): void + { + $reg = $this->registry(); + + $this->assertSame(50, $reg->get('lobby')->limit); + $this->assertNull($reg->get('just-talk')->limit); + + // Explicit type wins… + $this->assertSame(RoomType::Presence, $reg->get('lobby')->roomType()); + // …otherwise it is derived from the id prefix. + $this->assertSame(RoomType::Private, $reg->get('private-desk')->roomType()); + $this->assertSame(RoomType::Public, $reg->get('just-talk')->roomType()); + } + + public function test_meta_carries_unknown_keys_for_the_app(): void + { + $reg = new ConfigRoomRegistry([ + 'lobby' => ['mode' => 'sfu', 'label' => 'Main lobby', 'icon' => 'fa-comments'], + ]); + + $this->assertSame(['label' => 'Main lobby', 'icon' => 'fa-comments'], $reg->get('lobby')->meta); + } + + public function test_all_returns_every_definition_keyed_by_id(): void + { + $reg = $this->registry(); + + $this->assertSame( + ['just-talk', 'lobby', 'private-desk', 'default-mode'], + array_keys($reg->all()), + ); + } +}