diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -280,16 +280,12 @@ // Initialize connection tokens if ($init) { // Choose the connection role - if ($isOwner) { - $role = Room::ROLE_MODERATOR; - } elseif (request()->input('role') === Room::ROLE_PUBLISHER) { - $role = Room::ROLE_PUBLISHER; - } else { - $role = Room::ROLE_SUBSCRIBER; - } + $canPublish = !empty(request()->input('canPublish')); + $reqRole = $canPublish ? Room::ROLE_PUBLISHER : Room::ROLE_SUBSCRIBER; + $role = $isOwner ? Room::ROLE_MODERATOR : $reqRole; // Create session token for the current user/connection - $response = $room->getSessionToken($role); + $response = $room->getSessionToken($role, ['canPublish' => $canPublish]); if (empty($response)) { return $this->errorResponse(500, \trans('meet.session-join-error')); @@ -306,6 +302,7 @@ $response['role'] = $role; $response['owner'] = $isOwner; $response['config'] = $config; + $response['canPublish'] = $canPublish; } else { $response_code = 422; $response['code'] = 322; diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -297,6 +297,7 @@ DB::beginTransaction(); + // @phpstan-ignore-next-line if ($this->deleteBeforeCreate) { $this->deleteBeforeCreate->forceDelete(); } diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -169,19 +169,34 @@ /** * Create a OpenVidu session (connection) token * + * @param string $role User role + * @param array $data User data to attach to the connection. + * It will be available client-side for everybody. + * * @return array|null Token data on success, NULL otherwise * @throws \Exception if session does not exist */ - public function getSessionToken($role = self::ROLE_PUBLISHER): ?array + public function getSessionToken($role = self::ROLE_PUBLISHER, $data = []): ?array { if (!$this->session_id) { throw new \Exception("The room session does not exist"); } + // FIXME: Looks like passing the role in 'data' param is the only way + // to make it visible for everyone in a room. So, for example we can + // handle/style subscribers/publishers/moderators differently on the + // client-side. Is this a security issue? + if (!empty($data)) { + $data += ['role' => $role]; + } else { + $data = ['role' => $role]; + } + $url = 'sessions/' . $this->session_id . '/connection'; $post = [ 'json' => [ - 'role' => $role + 'role' => $role, + 'data' => json_encode($data) ] ]; diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js --- a/src/resources/js/meet/app.js +++ b/src/resources/js/meet/app.js @@ -37,6 +37,7 @@ let chatCount = 0 let volumeElement let setupProps + let subscribersContainer OV = new OpenVidu() screenOV = new OpenVidu() @@ -70,8 +71,8 @@ /** * Join the room session * - * @param data Session metadata and event handlers (session, token, shareToken, nickname, role, - * chatElement, menuElement, onDestroy, onJoinRequest) + * @param data Session metadata and event handlers (session, token, shareToken, nickname, + * canPublish, chatElement, menuElement, onDestroy, onJoinRequest) */ function joinRoom(data) { resize(); @@ -82,6 +83,11 @@ // avatar: undefined // avatar image } + // Create a container for subscribers + if (!subscribersContainer) { + subscribersContainer = $('
').appendTo(container).get(0) + } + sessionData = data // Init a session @@ -96,25 +102,20 @@ } // This is the first event executed when a user joins in. - // We'll create the video wrapper here, which will be re-used + // We'll create the video wrapper here, which can be re-used // in 'streamCreated' event handler. - // Note: For a user with no cam/mic enabled streamCreated event + // Note: For a user with a subscriber role 'streamCreated' event // is not being dispatched at all - // TODO: We may consider placing users with no video enabled - // in a separate place, so they do not fill the precious - // screen estate - + let metadata = connectionData(event.connection) let connectionId = event.connection.connectionId - let metadata = JSON.parse(event.connection.data) metadata.connId = connectionId - let wrapper = videoWrapperCreate(container, metadata) - connections[connectionId] = { - element: wrapper - } + let element = participantCreate(metadata) - updateLayout() + connections[connectionId] = { element } + + resize() // Send the current user status to the connecting user // otherwise e.g. nickname might be not up to date @@ -124,18 +125,20 @@ session.on('connectionDestroyed', event => { let conn = connections[event.connection.connectionId] if (conn) { + if ($(conn.element).is('.meet-video')) { + numOfVideos-- + } $(conn.element).remove() - numOfVideos-- - updateLayout() delete connections[event.connection.connectionId] } + resize() }) // On every new Stream received... session.on('streamCreated', event => { let connection = event.stream.connection let connectionId = connection.connectionId - let metadata = JSON.parse(connection.data) + let metadata = connectionData(connection) let wrapper = connections[connectionId].element let props = { // Prepend the video element so it is always before the watermark element @@ -150,14 +153,14 @@ tabindex: -1 }) - updateLayout() + resize() }) /* subscriber.on('videoElementDestroyed', event => { }) */ // Update the wrapper controls/status - videoWrapperUpdate(wrapper, event.stream) + participantUpdate(wrapper, event.stream) }) /* session.on('streamDestroyed', event => { @@ -169,7 +172,7 @@ data.onDestroy(event) } - updateLayout() + resize() }) // Handle signals from all participants @@ -178,8 +181,9 @@ // Connect with the token session.connect(data.token, data.params) .then(() => { - let params = { publisher: true, audioActive, videoActive } - let wrapper = videoWrapperCreate(container, Object.assign({}, data.params, params)) + let wrapper + let params = { self: true, canPublish: data.canPublish, audioActive, videoActive } + params = Object.assign({}, data.params, params) publisher.on('videoElementCreated', event => { $(event.element).prop({ @@ -187,17 +191,18 @@ disablePictureInPicture: true, // this does not work in Firefox tabindex: -1 }) - updateLayout() + resize() }) - publisher.createVideoElement(wrapper, 'PREPEND') - - sessionData.wrapper = wrapper + wrapper = participantCreate(params) - // Publish the stream - if (sessionData.role != 'SUBSCRIBER') { + if (data.canPublish) { + publisher.createVideoElement(wrapper, 'PREPEND') session.publish(publisher) } + + resize() + sessionData.wrapper = wrapper }) .catch(error => { console.error('There was an error connecting to the session: ', error.message); @@ -427,7 +432,7 @@ if (conn = connections[connId]) { data = JSON.parse(signal.data) - videoWrapperUpdate(conn.element, data) + participantUpdate(conn.element, data) nicknameUpdate(data.nickname, connId) } break @@ -571,7 +576,7 @@ try { publisher.publishAudio(!audioActive) audioActive = !audioActive - videoWrapperUpdate(sessionData.wrapper, { audioActive }) + participantUpdate(sessionData.wrapper, { audioActive }) signalUserUpdate() } catch (e) { console.error(e) @@ -593,7 +598,7 @@ try { publisher.publishVideo(!videoActive) videoActive = !videoActive - videoWrapperUpdate(sessionData.wrapper, { videoActive }) + participantUpdate(sessionData.wrapper, { videoActive }) signalUserUpdate() } catch (e) { console.error(e) @@ -649,24 +654,32 @@ } /** + * Create a participant element in the matrix. Depending on the `canPublish` + * parameter it will be a video element wrapper inside the matrix or a simple + * tag-like element on the subscribers list. + * + * @param params Connection metadata/params + * + * @return The element + */ + function participantCreate(params) { + if (params.canPublish) { + return publisherCreate(params) + } + + return subscriberCreate(params) + } + + /** * Create a