Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117816177
D2149.1775285342.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
43 KB
Referenced Files
None
Subscribers
None
D2149.1775285342.diff
View Options
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
@@ -12,7 +12,7 @@
class OpenViduController extends Controller
{
/**
- * Accepting the room join request.
+ * Accept the room join request.
*
* @param string $id Room identifier (name)
* @param string $reqid Request identifier
@@ -28,10 +28,8 @@
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
- $user = Auth::guard()->user();
-
- // Only the room owner can do it
- if (!$user || $user->id != $room->user_id) {
+ // Only the moderator can do it
+ if (!$this->isModerator($room)) {
return $this->errorResponse(403);
}
@@ -43,7 +41,7 @@
}
/**
- * Denying the room join request.
+ * Deny the room join request.
*
* @param string $id Room identifier (name)
* @param string $reqid Request identifier
@@ -59,10 +57,8 @@
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
- $user = Auth::guard()->user();
-
- // Only the room owner can do it
- if (!$user || $user->id != $room->user_id) {
+ // Only the moderator can do it
+ if (!$this->isModerator($room)) {
return $this->errorResponse(403);
}
@@ -107,7 +103,7 @@
}
/**
- * Accepting the room join request.
+ * Dismiss the participant/connection from the session.
*
* @param string $id Room identifier (name)
* @param string $conn Connection identifier
@@ -123,10 +119,8 @@
return $this->errorResponse(404, \trans('meet.connection-not-found'));
}
- $user = Auth::guard()->user();
-
- // Only the room owner can do it (for now)
- if (!$user || $user->id != $connection->room->user_id) {
+ // Only the moderator can do it
+ if (!$this->isModerator($connection->room)) {
return $this->errorResponse(403);
}
@@ -372,6 +366,42 @@
]);
}
+ /**
+ * Update the participant/connection parameters (e.g. role).
+ *
+ * @param string $id Room identifier (name)
+ * @param string $conn Connection identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function updateConnection($id, $conn)
+ {
+ $connection = Connection::where('id', $conn)->first();
+
+ // There's no such connection, bye bye
+ if (!$connection || $connection->room->name != $id) {
+ return $this->errorResponse(404, \trans('meet.connection-not-found'));
+ }
+
+ // Only the moderator can do it
+ if (!$this->isModerator($connection->room)) {
+ return $this->errorResponse(403);
+ }
+
+ foreach (request()->input() as $key => $value) {
+ switch ($key) {
+ case 'role':
+ $connection->{$key} = $value;
+ break;
+ }
+ }
+
+ // The connection observer will send a signal to everyone when needed
+ $connection->save();
+
+ return response()->json(['status' => 'success']);
+ }
+
/**
* Webhook as triggered from OpenVidu server
*
@@ -406,4 +436,25 @@
return response('Success', 200);
}
+
+ /**
+ * Check if current user is a moderator for the specified room.
+ *
+ * @param \App\OpenVidu\Room $room The room
+ *
+ * @return bool True if the current user is the room moderator
+ */
+ protected function isModerator(Room $room): bool
+ {
+ $user = Auth::guard()->user();
+
+ // The room owner is a moderator
+ if ($user && $user->id == $room->user_id) {
+ return true;
+ }
+
+ // TODO: Moderators authentication
+
+ return false;
+ }
}
diff --git a/src/app/Observers/OpenVidu/ConnectionObserver.php b/src/app/Observers/OpenVidu/ConnectionObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/OpenVidu/ConnectionObserver.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Observers\OpenVidu;
+
+use App\OpenVidu\Connection;
+
+class ConnectionObserver
+{
+ /**
+ * Handle the OpenVidu connection "updated" event.
+ *
+ * @param \App\OpenVidu\Connection $connection The connection.
+ *
+ * @return void
+ */
+ public function updated(Connection $connection)
+ {
+ if ($connection->role != $connection->getOriginal('role')) {
+ $params = [
+ 'connectionId' => $connection->id,
+ 'role' => $connection->role
+ ];
+
+ // Send the signal to all participants
+ $connection->room->signal('connectionUpdate', $params);
+
+ // TODO: When demoting publisher to subscriber maybe we should
+ // destroy all streams using REST API. For now we trust the
+ // participant browser to do this.
+ }
+ }
+}
diff --git a/src/app/OpenVidu/Connection.php b/src/app/OpenVidu/Connection.php
--- a/src/app/OpenVidu/Connection.php
+++ b/src/app/OpenVidu/Connection.php
@@ -54,4 +54,45 @@
{
return $this->belongsTo(Room::class, 'room_id', 'id');
}
+
+ /**
+ * Connection role mutator
+ *
+ * @throws \Exception
+ */
+ public function setRoleAttribute($role)
+ {
+ $new_role = 0;
+
+ $allowed_values = [
+ Room::ROLE_SUBSCRIBER,
+ Room::ROLE_PUBLISHER,
+ Room::ROLE_MODERATOR,
+ Room::ROLE_SCREEN,
+ Room::ROLE_OWNER,
+ ];
+
+ foreach ($allowed_values as $value) {
+ if ($role & $value) {
+ $new_role |= $value;
+ $role ^= $value;
+ }
+ }
+
+ if ($role > 0) {
+ throw new \Exception("Invalid connection role: {$role}");
+ }
+
+ // It is either screen sharing connection or publisher/subscriber connection
+ if ($new_role & Room::ROLE_SCREEN) {
+ if ($new_role & Room::ROLE_PUBLISHER) {
+ $new_role ^= Room::ROLE_PUBLISHER;
+ }
+ if ($new_role & Room::ROLE_SUBSCRIBER) {
+ $new_role ^= Room::ROLE_SUBSCRIBER;
+ }
+ }
+
+ $this->attributes['role'] = $new_role;
+ }
}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -30,6 +30,7 @@
\App\Domain::observe(\App\Observers\DomainObserver::class);
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
\App\Group::observe(\App\Observers\GroupObserver::class);
+ \App\OpenVidu\Connection::observe(\App\Observers\OpenVidu\ConnectionObserver::class);
\App\Package::observe(\App\Observers\PackageObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
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
@@ -17,7 +17,6 @@
let publisher // Publisher object which the user will publish
let audioActive = false // True if the audio track of the publisher is active
let videoActive = false // True if the video track of the publisher is active
- let numOfVideos = 0 // Keeps track of the number of videos that are being shown
let audioSource = '' // Currently selected microphone
let videoSource = '' // Currently selected camera
let sessionData // Room session metadata
@@ -76,8 +75,9 @@
/**
* 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 (token, shareToken, nickname, role,
+ * chatElement, menuElement, onDestroy, onJoinRequest, onDismiss, onConnectionChange,
+ * onSessionDataUpdate)
*/
function joinRoom(data) {
resize();
@@ -108,18 +108,12 @@
// This is the first event executed when a user joins in.
// We'll create the video wrapper here, which can be re-used
// in 'streamCreated' event handler.
- // Note: For a user with a subscriber role 'streamCreated' event
- // is not being dispatched at all
let metadata = connectionData(event.connection)
- let connectionId = event.connection.connectionId
- metadata.connId = connectionId
- let element = participantCreate(metadata)
+ metadata.element = participantCreate(metadata)
- connections[connectionId] = { element }
-
- resize()
+ connections[metadata.connectionId] = metadata
// Send the current user status to the connecting user
// otherwise e.g. nickname might be not up to date
@@ -129,9 +123,6 @@
session.on('connectionDestroyed', event => {
let conn = connections[event.connection.connectionId]
if (conn) {
- if ($(conn.element).is('.meet-video')) {
- numOfVideos--
- }
$(conn.element).remove()
delete connections[event.connection.connectionId]
}
@@ -140,17 +131,15 @@
// On every new Stream received...
session.on('streamCreated', event => {
- let connection = event.stream.connection
- let connectionId = connection.connectionId
- let metadata = connectionData(connection)
- let wrapper = connections[connectionId].element
+ let connectionId = event.stream.connection.connectionId
+ let metadata = connections[connectionId]
let props = {
// Prepend the video element so it is always before the watermark element
insertMode: 'PREPEND'
}
// Subscribe to the Stream to receive it
- let subscriber = session.subscribe(event.stream, wrapper, props);
+ let subscriber = session.subscribe(event.stream, metadata.element, props);
subscriber.on('videoElementCreated', event => {
$(event.element).prop({
@@ -159,17 +148,29 @@
resize()
})
-/*
- subscriber.on('videoElementDestroyed', event => {
- })
-*/
+
+ metadata.audioActive = event.stream.audioActive
+ metadata.videoActive = event.stream.videoActive
+
// Update the wrapper controls/status
- participantUpdate(wrapper, event.stream)
+ participantUpdate(metadata.element, metadata)
})
-/*
- session.on('streamDestroyed', event => {
+
+ // Stream properties changes e.g. audio/video muted/unmuted
+ session.on('streamPropertyChanged', event => {
+ let connectionId = event.stream.connection.connectionId
+ let metadata = connections[connectionId]
+
+ if (session.connection.connectionId == connectionId) {
+ metadata = sessionData
+ }
+
+ if (metadata) {
+ metadata[event.changedProperty] = event.newValue
+ participantUpdate(metadata.element, metadata)
+ }
})
-*/
+
// Handle session disconnection events
session.on('sessionDisconnected', event => {
if (data.onDestroy) {
@@ -185,8 +186,13 @@
// Connect with the token
session.connect(data.token, data.params)
.then(() => {
- let wrapper
- let params = { self: true, role: data.role, audioActive, videoActive }
+ let params = {
+ connectionId: session.connection.connectionId,
+ role: data.role,
+ audioActive,
+ videoActive
+ }
+
params = Object.assign({}, data.params, params)
publisher.on('videoElementCreated', event => {
@@ -198,15 +204,14 @@
resize()
})
- wrapper = participantCreate(params)
+ let wrapper = participantCreate(params)
if (data.role & Roles.PUBLISHER) {
publisher.createVideoElement(wrapper, 'PREPEND')
session.publish(publisher)
}
- resize()
- sessionData.wrapper = wrapper
+ sessionData.element = wrapper
})
.catch(error => {
console.error('There was an error connecting to the session: ', error.message);
@@ -433,10 +438,12 @@
switch (signal.type) {
case 'signal:userChanged':
+ // TODO: Use 'signal:connectionUpdate' for nickname updates?
if (conn = connections[connId]) {
data = JSON.parse(signal.data)
- participantUpdate(conn.element, data)
+ conn.nickname = data.nickname
+ participantUpdate(conn.element, conn)
nicknameUpdate(data.nickname, connId)
}
break
@@ -448,10 +455,20 @@
break
case 'signal:joinRequest':
- if (sessionData.onJoinRequest) {
+ // accept requests from the server only
+ if (!connId && sessionData.onJoinRequest) {
sessionData.onJoinRequest(JSON.parse(signal.data))
}
- break;
+ break
+
+ case 'signal:connectionUpdate':
+ // accept requests from the server only
+ if (!connId) {
+ data = JSON.parse(signal.data)
+
+ connectionUpdate(data)
+ }
+ break
}
}
@@ -542,14 +559,9 @@
*/
function signalUserUpdate(connection) {
let data = {
- audioActive,
- videoActive,
nickname: sessionData.params.nickname
}
- // Note: StreamPropertyChangedEvent might be more standard way
- // to propagate the audio/video state change to other users.
- // It looks there's no other way to propagate nickname changes.
session.signal({
data: JSON.stringify(data),
type: 'userChanged',
@@ -558,8 +570,6 @@
// The same nickname for screen sharing session
if (screenSession) {
- data.audioActive = false
- data.videoActive = true
screenSession.signal({
data: JSON.stringify(data),
type: 'userChanged',
@@ -580,8 +590,6 @@
try {
publisher.publishAudio(!audioActive)
audioActive = !audioActive
- participantUpdate(sessionData.wrapper, { audioActive })
- signalUserUpdate()
} catch (e) {
console.error(e)
}
@@ -602,8 +610,6 @@
try {
publisher.publishVideo(!videoActive)
videoActive = !videoActive
- participantUpdate(sessionData.wrapper, { videoActive })
- signalUserUpdate()
} catch (e) {
console.error(e)
}
@@ -640,6 +646,65 @@
return !!OV.checkScreenSharingCapabilities();
}
+ /**
+ * Update participant connection state
+ */
+ function connectionUpdate(data) {
+ let conn = connections[data.connectionId]
+
+ // It's me
+ if (session.connection.connectionId == data.connectionId) {
+ const rolePublisher = data.role && data.role & Roles.PUBLISHER
+ const isPublisher = sessionData.role & Roles.PUBLISHER
+
+ // Inform the vue component, so it can update some UI controls
+ let update = () => {
+ if (sessionData.onSessionDataUpdate) {
+ sessionData.onSessionDataUpdate(data)
+ }
+ }
+
+ // demoted to a subscriber
+ if ('role' in data && isPublisher && !rolePublisher) {
+ session.unpublish(publisher)
+ // FIXME: There's a reference in OpenVidu to a video element that should not
+ // exist anymore. It causes issues when we try to do publish/unpublish
+ // sequence multiple times in a row. So, we're clearing the reference here.
+ let videos = publisher.stream.streamManager.videos
+ publisher.stream.streamManager.videos = videos.filter(video => video.video.parentNode != null)
+ }
+
+ // merge the changed data into internal session metadata object
+ Object.keys(data).forEach(key => { sessionData[key] = data[key] })
+
+ // update the participant element
+ sessionData.element = participantUpdate(sessionData.element, sessionData)
+
+ // promoted to a publisher
+ if ('role' in data && !isPublisher && rolePublisher) {
+ publisher.createVideoElement(sessionData.element, 'PREPEND')
+ session.publish(publisher).then(() => {
+ data.audioActive = publisher.stream.audioActive
+ data.videoActive = publisher.stream.videoActive
+ update()
+ })
+
+ // TODO: Here the user is asked for media permissions again
+ // should we rather start the stream without asking the user?
+ // Or maybe we want to display the media setup/preview form?
+ // Need to find a way to do this.
+ } else {
+ // Inform the vue component, so it can update some UI controls
+ update()
+ }
+ } else if (conn) {
+ // merge the changed data into internal session metadata object
+ Object.keys(data).forEach(key => { conn[key] = data[key] })
+
+ conn.element = participantUpdate(conn.element, conn)
+ }
+ }
+
/**
* Update nickname in chat
*
@@ -667,11 +732,19 @@
* @return The element
*/
function participantCreate(params) {
+ let element
+
+ params.isSelf = params.isSelf || session.connection.connectionId == params.connectionId
+
if (params.role & Roles.PUBLISHER || params.role & Roles.SCREEN) {
- return publisherCreate(params)
+ element = publisherCreate(params)
+ } else {
+ element = subscriberCreate(params)
}
- return subscriberCreate(params)
+ setTimeout(resize, 50);
+
+ return element
}
/**
@@ -699,7 +772,7 @@
// Append the nickname widget
wrapper.find('.controls').before(nicknameWidget(params))
- if (!params.self) {
+ if (!params.isSelf) {
// Enable audio mute button
wrapper.find('.link-audio').removeClass('hidden')
.on('click', e => {
@@ -731,12 +804,12 @@
})
}
- numOfVideos++
-
// Remove the subscriber element, if exists
- $('#subscriber-' + params.connId).remove()
+ $('#subscriber-' + params.connectionId).remove()
- return wrapper[params.self ? 'prependTo' : 'appendTo'](container).get(0)
+ return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container)
+ .attr('id', 'publisher-' + params.connectionId)
+ .get(0)
}
/**
@@ -746,27 +819,51 @@
* @param params Connection metadata/params
*/
function participantUpdate(wrapper, params) {
- const $element = $(wrapper)
+ const element = $(wrapper)
+ const isModerator = sessionData.role & Roles.MODERATOR
+ const isSelf = session.connection.connectionId == params.connectionId
+
+ // Handle publisher-to-subscriber and subscriber-to-publisher change
+ if ('role' in params && !(params.role & Roles.SCREEN)) {
+ const rolePublisher = params.role & Roles.PUBLISHER
+ const isPublisher = element.is('.meet-video')
+
+ if ((rolePublisher && !isPublisher) || (!rolePublisher && isPublisher)) {
+ element.remove()
+ return participantCreate(params)
+ }
+
+ element.find('.action-role-publisher input').prop('checked', params.role & Roles.PUBLISHER)
+ }
if ('audioActive' in params) {
- $element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden')
+ element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden')
}
if ('videoActive' in params) {
- $element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden')
+ element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden')
}
if ('nickname' in params) {
- $element.find('.meet-nickname > .content').text(params.nickname)
+ element.find('.meet-nickname > .content').text(params.nickname)
+ }
+
+ if (isSelf) {
+ element.addClass('self')
}
- if (params.self) {
- $element.addClass('self')
+ if (isModerator) {
+ element.addClass('moderated')
}
- if (sessionData.role & Roles.MODERATOR) {
- $element.addClass('moderated')
+ element.find('.dropdown-menu')[isSelf || isModerator ? 'removeClass' : 'addClass']('hidden')
+ element.find('.permissions')[isModerator ? 'removeClass' : 'addClass']('hidden')
+
+ if ('role' in params && params.role & Roles.SCREEN) {
+ element.find('.permissions').addClass('hidden')
}
+
+ return wrapper
}
/**
@@ -780,8 +877,8 @@
participantUpdate(wrapper, params)
- return wrapper[params.self ? 'prependTo' : 'appendTo'](subscribersContainer)
- .attr('id', 'subscriber-' + params.connId)
+ return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](subscribersContainer)
+ .attr('id', 'subscriber-' + params.connectionId)
.get(0)
}
@@ -794,20 +891,35 @@
// Create the element
let element = $(
'<div class="dropdown">'
- + '<a href="#" class="meet-nickname btn" title="Nickname" aria-haspopup="true" aria-expanded="false" role="button">'
+ + '<a href="#" class="meet-nickname btn" aria-haspopup="true" aria-expanded="false" role="button">'
+ '<span class="content"></span>'
+ '<span class="icon">' + svgIcon('user') + '</span>'
+ '</a>'
+ '<div class="dropdown-menu">'
+ + '<a class="dropdown-item action-nickname" href="#">Nickname</a>'
+ '<a class="dropdown-item action-dismiss" href="#">Dismiss</a>'
+ + '<div class="dropdown-divider permissions"></div>'
+ + '<div class="permissions">'
+ + '<h6 class="dropdown-header">Permissions</h6>'
+ + '<label class="dropdown-item action-role-publisher custom-control custom-switch">'
+ + '<input type="checkbox" class="custom-control-input">'
+ + ' <span class="custom-control-label">Audio & Video publishing</span>'
+ + '</label>'
+ //+ '<label class="dropdown-item action-role-moderator custom-control custom-switch">'
+ // + '<input type="checkbox" class="custom-control-input">'
+ // + ' <span class="custom-control-label">Moderation</span>'
+ //+ '</label>'
+ + '</div>'
+ '</div>'
+ '</div>'
)
let nickname = element.find('.meet-nickname')
- .addClass('btn btn-outline-' + (params.self ? 'primary' : 'secondary'))
+ .addClass('btn btn-outline-' + (params.isSelf ? 'primary' : 'secondary'))
+ .attr({title: 'Options', 'data-toggle': 'dropdown'})
+ .dropdown({boundary: container})
- if (params.self) {
+ if (params.isSelf) {
// Add events for nickname change
let editable = element.find('.content')[0]
let editableEnable = () => {
@@ -821,7 +933,8 @@
nicknameUpdate(editable.innerText, session.connection.connectionId)
}
- nickname.on('click', editableEnable)
+ element.find('.action-nickname').on('click', editableEnable)
+ element.find('.action-dismiss').remove()
$(editable).on('blur', editableUpdate)
.on('keydown', e => {
@@ -831,17 +944,50 @@
return false
}
})
- } else if (sessionData.role & Roles.MODERATOR) {
- nickname.attr({title: 'Options', 'data-toggle': 'dropdown'})
- .dropdown({boundary: container})
+ } else {
+ element.find('.action-nickname').remove()
element.find('.action-dismiss').on('click', e => {
if (sessionData.onDismiss) {
- sessionData.onDismiss(params.connId)
+ sessionData.onDismiss(params.connectionId)
}
})
}
+ // Don't close the menu on permission change
+ element.find('.dropdown-menu > label').on('click', e => { e.stopPropagation() })
+
+ if (sessionData.onConnectionChange) {
+ element.find('.action-role-publisher input').on('change', e => {
+ const enabled = e.target.checked
+ let role = params.role
+
+ if (enabled) {
+ role |= Roles.PUBLISHER
+ } else {
+ role |= Roles.SUBSCRIBER
+ if (role & Roles.PUBLISHER) {
+ role ^= Roles.PUBLISHER
+ }
+ }
+
+ sessionData.onConnectionChange(params.connectionId, { role })
+ })
+
+ element.find('.action-role-moderator input').on('change', e => {
+ const enabled = e.target.checked
+ let role = params.role
+
+ if (enabled) {
+ role |= Roles.MODERATOR
+ } else if (role & Roles.MODERATOR) {
+ role ^= Roles.MODERATOR
+ }
+
+ sessionData.onConnectionChange(params.connectionId, { role })
+ })
+ }
+
return element.get(0)
}
@@ -864,6 +1010,7 @@
* Update the room "matrix" layout
*/
function updateLayout() {
+ let numOfVideos = $(container).find('.meet-video').length
if (!numOfVideos) {
return
}
@@ -1071,7 +1218,11 @@
// OpenVidu is unable to merge these two objects into one, for it it is only
// two strings, so it puts a "%/%" separator in between, we'll replace it with comma
// to get one parseable json object
- return JSON.parse(connection.data.replace('}%/%{', ','))
+ let data = JSON.parse(connection.data.replace('}%/%{', ','))
+
+ data.connectionId = connection.connectionId
+
+ return data
}
}
diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss
--- a/src/resources/themes/meet.scss
+++ b/src/resources/themes/meet.scss
@@ -35,6 +35,13 @@
}
}
}
+
+ & + .dropdown-menu {
+ .permissions > label {
+ margin: 0;
+ padding-left: 3.75rem;
+ }
+ }
}
.meet-video {
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -316,6 +316,9 @@
$('#meet-session-menu').find('.link-fullscreen.closed').removeClass('hidden')
}
},
+ isModerator() {
+ return this.isRoomOwner() || (!!this.session.role && (this.session.role & Roles.MODERATOR) > 0)
+ },
isPublisher() {
return !!this.session.role && (this.session.role & Roles.PUBLISHER) > 0
},
@@ -414,12 +417,10 @@
}).modal()
}
}
-
this.session.onDismiss = connId => { this.dismissParticipant(connId) }
-
- if (this.isRoomOwner()) {
- this.session.onJoinRequest = data => { this.joinRequest(data) }
- }
+ this.session.onSessionDataUpdate = data => { this.updateSession(data) }
+ this.session.onConnectionChange = (connId, data) => { this.updateParticipant(connId, data) }
+ this.session.onJoinRequest = data => { this.joinRequest(data) }
this.meet.joinRoom(this.session)
},
@@ -570,14 +571,43 @@
if (!enabled) {
// TODO: This might need to be a different route. E.g. the room password might have
// changed since user joined the session
+ // Also because it creates a redundant connection (token)
axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
.then(response => {
// Response data contains: session, token and shareToken
- this.session.shareToken = response.data.token
+ this.session.shareToken = response.data.shareToken
this.meet.updateSession(this.session)
})
}
})
+ },
+ updateParticipant(connId, params) {
+ if (this.isModerator()) {
+ axios.put('/api/v4/openvidu/rooms/' + this.room + '/connections/' + connId, params)
+ }
+ },
+ updateSession(data) {
+ let params = {}
+
+ if ('role' in data) {
+ params.role = data.role
+ }
+
+ // merge new params into the object
+ this.session = Object.assign({}, this.session, params)
+
+ // update some buttons state e.g. when switching from publisher to subscriber
+ if (!this.isPublisher()) {
+ this.setMenuItem('audio', false)
+ this.setMenuItem('video', false)
+ } else {
+ if ('videoActive' in data) {
+ this.setMenuItem('video', data.videoActive)
+ }
+ if ('audioActive' in data) {
+ this.setMenuItem('audio', data.audioActive)
+ }
+ }
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -86,6 +86,7 @@
Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
// FIXME: I'm not sure about this one, should we use DELETE request maybe?
Route::post('openvidu/rooms/{id}/connections/{conn}/dismiss', 'API\V4\OpenViduController@dismissConnection');
+ Route::put('openvidu/rooms/{id}/connections/{conn}', 'API\V4\OpenViduController@updateConnection');
Route::post('openvidu/rooms/{id}/request/{reqid}/accept', 'API\V4\OpenViduController@acceptJoinRequest');
Route::post('openvidu/rooms/{id}/request/{reqid}/deny', 'API\V4\OpenViduController@denyJoinRequest');
}
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
--- a/src/tests/Browser/Meet/RoomControlsTest.php
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -178,7 +178,6 @@
// Test nickname change propagation
- // Use script() because type() does not work with this contenteditable widget
$guest->setNickname('div.meet-video.self', 'guest');
$owner->waitFor('div.meet-video:not(.self) .meet-nickname')
->assertSeeIn('div.meet-video:not(.self) .meet-nickname', 'guest');
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -370,4 +370,125 @@
$browser->waitUntilMissing('@session .meet-subscriber:not(.self)');
});
}
+
+ /**
+ * Test demoting publisher to a subscriber
+ *
+ * @group openvidu
+ * @depends testSubscribers
+ */
+ public function testDemoteToSubscriber(): void
+ {
+ $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+ $this->browse(function (Browser $browser, Browser $guest) {
+ // Join the room as the owner
+ $browser->visit(new RoomPage('john'))
+ ->waitFor('@setup-form')
+ ->waitUntilMissing('@setup-status-message.loading')
+ ->waitFor('@setup-status-message')
+ ->type('@setup-nickname-input', 'john')
+ ->clickWhenEnabled('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->waitFor('@session video');
+
+ // In one browser window act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->waitUntilMissing('@setup-status-message', 10)
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->clickWhenEnabled('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->waitFor('div.meet-video.self')
+ ->waitFor('div.meet-video:not(.self)')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->assertElementsCount('@session video', 2)
+ ->assertElementsCount('@session div.meet-subscriber', 0)
+ // assert there's no moderator-related features for this guess available
+ ->click('@session .meet-video.self .meet-nickname')
+ ->whenAvailable('@session .meet-video.self .dropdown-menu', function (Browser $browser) {
+ $browser->assertMissing('.permissions');
+ })
+ ->click('@session .meet-video:not(.self) .meet-nickname')
+ ->pause(50)
+ ->assertMissing('.dropdown-menu');
+
+ // Demote the guest to a subscriber
+ $browser
+ ->waitFor('div.meet-video.self')
+ ->waitFor('div.meet-video:not(.self)')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->assertElementsCount('@session video', 2)
+ ->assertElementsCount('@session .meet-subscriber', 0)
+ ->click('@session .meet-video:not(.self) .meet-nickname')
+ ->whenAvailable('@session .meet-video:not(.self) .dropdown-menu', function (Browser $browser) {
+ $browser->assertSeeIn('.action-role-publisher', 'Audio & Video publishing')
+ ->click('.action-role-publisher')
+ ->waitUntilMissing('.dropdown-menu');
+ })
+ ->waitUntilMissing('@session .meet-video:not(.self)')
+ ->waitFor('@session div.meet-subscriber')
+ ->assertElementsCount('@session div.meet-video', 1)
+ ->assertElementsCount('@session video', 1)
+ ->assertElementsCount('@session div.meet-subscriber', 1);
+
+ $guest
+ ->waitUntilMissing('@session .meet-video.self')
+ ->waitFor('@session div.meet-subscriber')
+ ->assertElementsCount('@session div.meet-video', 1)
+ ->assertElementsCount('@session video', 1)
+ ->assertElementsCount('@session div.meet-subscriber', 1);
+
+ // Promote the guest back to a publisher
+ $browser
+ ->click('@session .meet-subscriber .meet-nickname')
+ ->whenAvailable('@session .meet-subscriber .dropdown-menu', function (Browser $browser) {
+ $browser->assertSeeIn('.action-role-publisher', 'Audio & Video publishing')
+ ->assertNotChecked('.action-role-publisher input')
+ ->click('.action-role-publisher')
+ ->waitUntilMissing('.dropdown-menu');
+ })
+ ->waitFor('@session .meet-video:not(.self)')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->assertElementsCount('@session video', 2)
+ ->assertElementsCount('@session div.meet-subscriber', 0);
+
+ $guest
+ ->waitFor('@session .meet-video.self')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->assertElementsCount('@session video', 2)
+ ->assertElementsCount('@session div.meet-subscriber', 0);
+
+ // Demote the owner to a subscriber
+ $browser
+ ->click('@session .meet-video.self .meet-nickname')
+ ->whenAvailable('@session .meet-video.self .dropdown-menu', function (Browser $browser) {
+ $browser->assertSeeIn('.action-role-publisher', 'Audio & Video publishing')
+ ->assertChecked('.action-role-publisher input')
+ ->click('.action-role-publisher')
+ ->waitUntilMissing('.dropdown-menu');
+ })
+ ->waitUntilMissing('@session .meet-video.self')
+ ->waitFor('@session div.meet-subscriber.self')
+ ->assertElementsCount('@session div.meet-video', 1)
+ ->assertElementsCount('@session video', 1)
+ ->assertElementsCount('@session div.meet-subscriber', 1);
+
+ // Promote the owner to a publisher
+ $browser
+ ->click('@session .meet-subscriber.self .meet-nickname')
+ ->whenAvailable('@session .meet-subscriber.self .dropdown-menu', function (Browser $browser) {
+ $browser->assertSeeIn('.action-role-publisher', 'Audio & Video publishing')
+ ->assertNotChecked('.action-role-publisher input')
+ ->click('.action-role-publisher')
+ ->waitUntilMissing('.dropdown-menu');
+ })
+ ->waitUntilMissing('@session .meet-subscriber.self')
+ ->waitFor('@session div.meet-video.self')
+ ->assertElementsCount('@session div.meet-video', 2)
+ ->assertElementsCount('@session video', 2)
+ ->assertElementsCount('@session div.meet-subscriber', 0);
+ });
+ }
}
diff --git a/src/tests/Browser/Pages/Meet/Room.php b/src/tests/Browser/Pages/Meet/Room.php
--- a/src/tests/Browser/Pages/Meet/Room.php
+++ b/src/tests/Browser/Pages/Meet/Room.php
@@ -161,14 +161,19 @@
*/
public function setNickname($browser, $selector, $nickname): void
{
- // Use script() because type() does not work with this contenteditable widget
- $selector = $selector . ' .meet-nickname .content';
- $browser->script(
- "var element = document.querySelector('$selector');"
- . "element.focus();"
- . "element.innerText = '$nickname';"
- . "element.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 }))"
- );
+ $element = "$selector .meet-nickname .content";
+
+ $browser->click("$selector .meet-nickname")
+ ->waitFor("$selector .dropdown-menu")
+ ->assertSeeIn("$selector .dropdown-menu > .action-nickname", 'Nickname')
+ ->click("$selector .dropdown-menu > .action-nickname")
+ ->waitUntilMissing('.dropdown-menu')
+ // Use script() because type() does not work with this contenteditable widget
+ ->script(
+ "var element = document.querySelector('$element');"
+ . "element.innerText = '$nickname';"
+ . "element.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 }))"
+ );
}
/**
diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php
--- a/src/tests/Feature/Controller/OpenViduTest.php
+++ b/src/tests/Feature/Controller/OpenViduTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature\Controller;
use App\Http\Controllers\API\V4\OpenViduController;
+use App\OpenVidu\Connection;
use App\OpenVidu\Room;
use Tests\TestCase;
@@ -554,4 +555,66 @@
$room->refresh();
$this->assertSame(null, $room->getSetting('password'));
}
+
+ /**
+ * Test updating a participant (connection)
+ *
+ * @group openvidu
+ */
+ public function testUpdateConnection(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = Room::where('name', 'john')->first();
+ $room->session_id = null;
+ $room->save();
+
+ $this->assignBetaEntitlement($john, 'meet');
+
+ // First we create the session
+ $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ // And the other user connection
+ $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $conn_id = $json['connectionId'];
+ $room->refresh();
+ $conn_data = $room->getOVConnection($conn_id);
+
+ $this->assertSame($conn_id, $conn_data['connectionId']);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/non-existing/connections/{$conn_id}", []);
+ $response->assertStatus(404);
+
+ // Non-existing connection
+ $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/{$room->name}/connections/123", []);
+ $response->assertStatus(404);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame('The connection does not exist.', $json['message']);
+
+ // Non-owner access
+ $response = $this->actingAs($jack)->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", []);
+ $response->assertStatus(403);
+
+ // Expected success
+ $post = ['role' => Room::ROLE_PUBLISHER | Room::ROLE_MODERATOR];
+ $response = $this->actingAs($john)->put("api/v4/openvidu/rooms/{$room->name}/connections/{$conn_id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame($post['role'], Connection::find($conn_id)->role);
+ }
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 6:49 AM (9 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828431
Default Alt Text
D2149.1775285342.diff (43 KB)
Attached To
Mode
D2149: Publisher-to-subscriber and vice-versa
Attached
Detach File
Event Timeline