diff --git a/appinfo/routes.php b/appinfo/routes.php index a1c75b8f35f..83f76f137cd 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -122,6 +122,15 @@ 'token' => '^[a-z0-9]{4,30}$', ], ], + [ + 'name' => 'Call#updateCallFlags', + 'url' => '/api/{apiVersion}/call/{token}', + 'verb' => 'PUT', + 'requirements' => [ + 'apiVersion' => 'v(4)', + 'token' => '^[a-z0-9]{4,30}$', + ], + ], [ 'name' => 'Call#leaveCall', 'url' => '/api/{apiVersion}/call/{token}', diff --git a/docs/call.md b/docs/call.md index fb44d17cc0e..fba81dfe8fe 100644 --- a/docs/call.md +++ b/docs/call.md @@ -47,6 +47,26 @@ + `404 Not Found` When the user did not join the conversation before + `412 Precondition Failed` When the lobby is active and the user is not a moderator +## Update call flags + +* Method: `PUT` +* Endpoint: `/call/{token}` +* Data: + + field | type | Description + ---|---|--- + `flags` | int | Flags what streams are provided by the participant (see [Constants - Participant in-call flag](constants.md#participant-in-call-flag)) + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the user is not in the call + + `400 Bad Request` When the flags do not contain "in call" + + `403 Forbidden` When the conversation is read-only + + `404 Not Found` When the conversation could not be found for the participant + + `404 Not Found` When the user did not join the conversation before + + `412 Precondition Failed` When the lobby is active and the user is not a moderator + ## Leave a call (but staying in the conversation for future calls and chat) * Method: `DELETE` diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index e638ca8d481..c17d8f1abd0 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -126,6 +126,28 @@ public function joinCall(?int $flags): DataResponse { return new DataResponse(); } + /** + * @PublicPage + * @RequireParticipant + * + * @param int flags + * @return DataResponse + */ + public function updateCallFlags(int $flags): DataResponse { + $session = $this->participant->getSession(); + if (!$session instanceof Session) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $this->participantService->updateCallFlags($this->room, $this->participant, $flags); + } catch (\Exception $exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + /** * @PublicPage * @RequireParticipant diff --git a/lib/Room.php b/lib/Room.php index cb98869ae0f..41b83f2ba58 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -124,6 +124,8 @@ class Room { public const EVENT_AFTER_GUESTS_CLEAN = self::class . '::postCleanGuests'; public const EVENT_BEFORE_SESSION_JOIN_CALL = self::class . '::preSessionJoinCall'; public const EVENT_AFTER_SESSION_JOIN_CALL = self::class . '::postSessionJoinCall'; + public const EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS = self::class . '::preSessionUpdateCallFlags'; + public const EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS = self::class . '::postSessionUpdateCallFlags'; public const EVENT_BEFORE_SESSION_LEAVE_CALL = self::class . '::preSessionLeaveCall'; public const EVENT_AFTER_SESSION_LEAVE_CALL = self::class . '::postSessionLeaveCall'; public const EVENT_BEFORE_SIGNALING_PROPERTIES = self::class . '::beforeSignalingProperties'; diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 5af1c29f699..a502af98d1a 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -678,6 +678,29 @@ public function changeInCall(Room $room, Participant $participant, int $flags): } } + public function updateCallFlags(Room $room, Participant $participant, int $flags): void { + $session = $participant->getSession(); + if (!$session instanceof Session) { + return; + } + + if (!($session->getInCall() & Participant::FLAG_IN_CALL)) { + throw new \Exception('Participant not in call'); + } + + if (!($flags & Participant::FLAG_IN_CALL)) { + throw new \InvalidArgumentException('Invalid flags'); + } + + $event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall()); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS, $event); + + $session->setInCall($flags); + $this->sessionMapper->update($session); + + $this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $event); + } + public function markUsersAsMentioned(Room $room, array $userIds, int $messageId): void { $query = $this->connection->getQueryBuilder(); $query->update('talk_attendees') diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 074563ed17f..9ebec18bdc0 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -65,6 +65,7 @@ protected static function registerInternalSignaling(IEventDispatcher $dispatcher $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CONNECT, $listener); $dispatcher->addListener(Room::EVENT_AFTER_GUEST_CONNECT, $listener); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener); $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, $listener); @@ -241,6 +242,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher } }; $dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener); $dispatcher->addListener(Room::EVENT_AFTER_GUESTS_CLEAN, static function (RoomEvent $event) { diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 8c19031b5c5..a787805e85a 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -176,6 +176,16 @@ Signaling.Base.prototype.leaveCurrentRoom = function() { } } +Signaling.Base.prototype.updateCurrentCallFlags = function(flags) { + return new Promise((resolve, reject) => { + if (this.currentCallToken) { + this.updateCallFlags(this.currentCallToken, flags).then(() => { resolve() }).catch(reason => { reject(reason) }) + } else { + resolve() + } + }) +} + Signaling.Base.prototype.leaveCurrentCall = function() { return new Promise((resolve, reject) => { if (this.currentCallToken) { @@ -266,6 +276,27 @@ Signaling.Base.prototype._leaveCallSuccess = function(/* token */) { // Override in subclasses if necessary. } +Signaling.Base.prototype.updateCallFlags = function(token, flags) { + return new Promise((resolve, reject) => { + if (!token) { + reject(new Error()) + return + } + + axios.put(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }), { + flags: flags, + }) + .then(function() { + this.currentCallFlags = flags + this._trigger('updateCallFlags', [token, flags]) + resolve() + }.bind(this)) + .catch(function() { + reject(new Error()) + }) + }) +} + Signaling.Base.prototype.leaveCall = function(token, keepToken) { return new Promise((resolve, reject) => { if (!token) { @@ -343,7 +374,7 @@ Signaling.Internal.prototype.forceReconnect = function(newSession, flags) { // FIXME Naive reconnection routine; as the same session is kept peers // must be explicitly ended before the reconnection is forced. this.leaveCall(this.currentCallToken, true) - this.joinCall(this.currentCallToken) + this.joinCall(this.currentCallToken, this.currentCallFlags) } Signaling.Internal.prototype._sendMessageWithCallback = function(ev) { diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index b339b71712e..6b184823d96 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -714,7 +714,33 @@ export default function initWebRTC(signaling, _callParticipantCollection, _local // is established, but forcing a reconnection should be done only // once the connection was established. if (peer.pc.iceConnectionState !== 'new' && peer.pc.iceConnectionState !== 'checking') { - forceReconnect(signaling) + // Update the media flags if needed, as the renegotiation could + // have been caused by tracks being added or removed. + const audioSender = peer.pc.getSenders().find((sender) => sender.track && sender.track.kind === 'audio') + const videoSender = peer.pc.getSenders().find((sender) => sender.track && sender.track.kind === 'video') + + let flags = signaling.getCurrentCallFlags() + if (audioSender) { + flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO + } else { + flags &= ~PARTICIPANT.CALL_FLAG.WITH_AUDIO + } + if (videoSender) { + flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO + } else { + flags &= ~PARTICIPANT.CALL_FLAG.WITH_VIDEO + } + + // Negotiation is expected to be needed only when a new track is + // added to or removed from a peer. Therefore if the HPB is used + // the negotiation will be needed in the own peer, but if the + // HPB is not used it will be needed in all peers. However, in + // that case as soon as the forced reconnection is triggered all + // the peers will be cleared, so in practice there will be just + // one forced reconnection even if there are several peers. + // FIXME: despite all of the above this is a dirty and ugly hack + // that should be fixed with proper renegotiation. + forceReconnect(signaling, flags) } }) } @@ -942,16 +968,34 @@ export default function initWebRTC(signaling, _callParticipantCollection, _local } }) - webrtc.on('localTrackReplaced', function(newTrack /*, oldTrack, stream */) { - // Device disabled, nothing to do here. + webrtc.on('localTrackReplaced', function(newTrack, oldTrack/*, stream */) { + // Device disabled, just update the call flags. if (!newTrack) { + if (oldTrack && oldTrack.kind === 'audio') { + signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() & ~PARTICIPANT.CALL_FLAG.WITH_AUDIO) + } else if (oldTrack && oldTrack.kind === 'video') { + signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() & ~PARTICIPANT.CALL_FLAG.WITH_VIDEO) + } + return } // If the call was started with media the connections will be already - // established. If it has not started yet the connections will be - // established once started. - if (startedWithMedia || startedWithMedia === undefined) { + // established. The flags need to be updated if a device was enabled + // (but not if it was switched to another one). + if (startedWithMedia) { + if (newTrack.kind === 'audio' && !oldTrack) { + signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() | PARTICIPANT.CALL_FLAG.WITH_AUDIO) + } else if (newTrack.kind === 'video' && !oldTrack) { + signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() | PARTICIPANT.CALL_FLAG.WITH_VIDEO) + } + + return + } + + // If the call has not started with media yet the connections will be + // established once started, as well as the flags. + if (startedWithMedia === undefined) { return } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 698b4a98b55..57fa6a9d133 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -442,7 +442,7 @@ public function userLoadsAttendeeIdsInRoom(string $user, string $identifier, str } protected function sortAttendees(array $a1, array $a2): int { - if ($a1['participantType'] !== $a2['participantType']) { + if (array_key_exists('participantType', $a1) && array_key_exists('participantType', $a2) && $a1['participantType'] !== $a2['participantType']) { return $a1['participantType'] <=> $a2['participantType']; } if ($a1['actorType'] !== $a2['actorType']) { @@ -1083,6 +1083,24 @@ public function userJoinsCall(string $user, string $identifier, int $statusCode, } } + /** + * @Then /^user "([^"]*)" updates call flags in room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/ + * + * @param string $user + * @param string $identifier + * @param string $flags + * @param int $statusCode + * @param string $apiVersion + */ + public function userUpdatesCallFlagsInRoomTo(string $user, string $identifier, string $flags, int $statusCode, string $apiVersion): void { + $this->setCurrentUser($user); + $this->sendRequest( + 'PUT', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier], + new TableNode([['flags', $flags]]) + ); + $this->assertStatusCode($this->response, $statusCode); + } + /** * @Then /^user "([^"]*)" leaves call "([^"]*)" with (\d+) \((v4)\)$/ * diff --git a/tests/integration/features/callapi/update-call-flags.feature b/tests/integration/features/callapi/update-call-flags.feature new file mode 100644 index 00000000000..ff2bde498dd --- /dev/null +++ b/tests/integration/features/callapi/update-call-flags.feature @@ -0,0 +1,272 @@ +Feature: callapi/update-call-flags + Background: + Given user "owner" exists + And user "moderator" exists + And user "invited user" exists + And user "not invited but joined user" exists + + Scenario: all participants can update their call flags when in a call + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" joins room "public room" with 200 (v4) + And user "moderator" joins room "public room" with 200 (v4) + And user "invited user" joins room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "owner" joins call "public room" with 200 (v4) + And user "moderator" joins call "public room" with 200 (v4) + And user "invited user" joins call "public room" with 200 (v4) + And user "not invited but joined user" joins call "public room" with 200 (v4) + And user "guest moderator" joins call "public room" with 200 (v4) + And user "guest" joins call "public room" with 200 (v4) + When user "owner" updates call flags in room "public room" to "1" with 200 (v4) + And user "moderator" updates call flags in room "public room" to "1" with 200 (v4) + And user "invited user" updates call flags in room "public room" to "1" with 200 (v4) + And user "not invited but joined user" updates call flags in room "public room" to "1" with 200 (v4) + And user "guest moderator" updates call flags in room "public room" to "1" with 200 (v4) + And user "guest" updates call flags in room "public room" to "1" with 200 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 1 | + | users | moderator | 1 | + | users | invited user | 1 | + | users | not invited but joined user | 1 | + | guests | "guest moderator" | 1 | + | guests | "guest" | 1 | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 1 | + | users | moderator | 1 | + | users | invited user | 1 | + | users | not invited but joined user | 1 | + | guests | "guest moderator" | 1 | + | guests | "guest" | 1 | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 1 | + | users | moderator | 1 | + | users | invited user | 1 | + | users | not invited but joined user | 1 | + | guests | "guest moderator" | 1 | + | guests | "guest" | 1 | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 1 | + | users | moderator | 1 | + | users | invited user | 1 | + | users | not invited but joined user | 1 | + | guests | "guest moderator" | 1 | + | guests | "guest" | 1 | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 1 | + | users | moderator | 1 | + | users | invited user | 1 | + | users | not invited but joined user | 1 | + | guests | "guest moderator" | 1 | + | guests | "guest" | 1 | + + Scenario: update call flags with in call does not join the call + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" joins room "public room" with 200 (v4) + And user "moderator" joins room "public room" with 200 (v4) + And user "invited user" joins room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + When user "owner" updates call flags in room "public room" to "1" with 400 (v4) + And user "moderator" updates call flags in room "public room" to "1" with 400 (v4) + And user "invited user" updates call flags in room "public room" to "1" with 400 (v4) + And user "not invited but joined user" updates call flags in room "public room" to "1" with 400 (v4) + And user "guest moderator" updates call flags in room "public room" to "1" with 400 (v4) + And user "guest" updates call flags in room "public room" to "1" with 400 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 0 | + | users | moderator | 0 | + | users | invited user | 0 | + | users | not invited but joined user | 0 | + | guests | "guest moderator" | 0 | + | guests | "guest" | 0 | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 0 | + | users | moderator | 0 | + | users | invited user | 0 | + | users | not invited but joined user | 0 | + | guests | "guest moderator" | 0 | + | guests | "guest" | 0 | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 0 | + | users | moderator | 0 | + | users | invited user | 0 | + | users | not invited but joined user | 0 | + | guests | "guest moderator" | 0 | + | guests | "guest" | 0 | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 0 | + | users | moderator | 0 | + | users | invited user | 0 | + | users | not invited but joined user | 0 | + | guests | "guest moderator" | 0 | + | guests | "guest" | 0 | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 0 | + | users | moderator | 0 | + | users | invited user | 0 | + | users | not invited but joined user | 0 | + | guests | "guest moderator" | 0 | + | guests | "guest" | 0 | + + Scenario: update call flags with disconnected does not leave the call + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" joins room "public room" with 200 (v4) + And user "moderator" joins room "public room" with 200 (v4) + And user "invited user" joins room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "owner" joins call "public room" with 200 (v4) + And user "moderator" joins call "public room" with 200 (v4) + And user "invited user" joins call "public room" with 200 (v4) + And user "not invited but joined user" joins call "public room" with 200 (v4) + And user "guest moderator" joins call "public room" with 200 (v4) + And user "guest" joins call "public room" with 200 (v4) + When user "owner" updates call flags in room "public room" to "0" with 400 (v4) + And user "moderator" updates call flags in room "public room" to "0" with 400 (v4) + And user "invited user" updates call flags in room "public room" to "0" with 400 (v4) + And user "not invited but joined user" updates call flags in room "public room" to "0" with 400 (v4) + And user "guest moderator" updates call flags in room "public room" to "0" with 400 (v4) + And user "guest" updates call flags in room "public room" to "0" with 400 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + + Scenario: update call flags requires in call flag to be set + Given user "owner" creates room "public room" (v4) + | roomType | 3 | + | roomName | room | + And user "owner" adds user "moderator" to room "public room" with 200 (v4) + And user "owner" promotes "moderator" in room "public room" with 200 (v4) + And user "owner" adds user "invited user" to room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "guest moderator" joins room "public room" with 200 (v4) + And user "owner" promotes "guest moderator" in room "public room" with 200 (v4) + And user "guest" joins room "public room" with 200 (v4) + And user "owner" joins room "public room" with 200 (v4) + And user "moderator" joins room "public room" with 200 (v4) + And user "invited user" joins room "public room" with 200 (v4) + And user "not invited but joined user" joins room "public room" with 200 (v4) + And user "owner" joins call "public room" with 200 (v4) + And user "moderator" joins call "public room" with 200 (v4) + And user "invited user" joins call "public room" with 200 (v4) + And user "not invited but joined user" joins call "public room" with 200 (v4) + And user "guest moderator" joins call "public room" with 200 (v4) + And user "guest" joins call "public room" with 200 (v4) + When user "owner" updates call flags in room "public room" to "2" with 400 (v4) + And user "moderator" updates call flags in room "public room" to "2" with 400 (v4) + And user "invited user" updates call flags in room "public room" to "2" with 400 (v4) + And user "not invited but joined user" updates call flags in room "public room" to "2" with 400 (v4) + And user "guest moderator" updates call flags in room "public room" to "2" with 400 (v4) + And user "guest" updates call flags in room "public room" to "2" with 400 (v4) + Then user "owner" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "invited user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "not invited but joined user" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 | + And user "guest moderator" sees the following attendees in room "public room" with 200 (v4) + | actorType | actorId | inCall | + | users | owner | 7 | + | users | moderator | 7 | + | users | invited user | 7 | + | users | not invited but joined user | 7 | + | guests | "guest moderator" | 7 | + | guests | "guest" | 7 |