Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117879078
D2149.1775342774.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
37 KB
Referenced Files
None
Subscribers
None
D2149.1775342774.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,35 @@
{
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}");
+ }
+
+ $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();
@@ -112,12 +112,10 @@
// 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 }
+ connections[metadata.connectionId] = metadata
resize()
@@ -129,9 +127,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 +135,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 +152,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 +190,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,7 +208,7 @@
resize()
})
- wrapper = participantCreate(params)
+ let wrapper = participantCreate(params)
if (data.role & Roles.PUBLISHER) {
publisher.createVideoElement(wrapper, 'PREPEND')
@@ -206,7 +216,7 @@
}
resize()
- sessionData.wrapper = wrapper
+ sessionData.element = wrapper
})
.catch(error => {
console.error('There was an error connecting to the session: ', error.message);
@@ -433,10 +443,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 +460,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 +564,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 +575,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 +595,6 @@
try {
publisher.publishAudio(!audioActive)
audioActive = !audioActive
- participantUpdate(sessionData.wrapper, { audioActive })
- signalUserUpdate()
} catch (e) {
console.error(e)
}
@@ -602,8 +615,6 @@
try {
publisher.publishVideo(!videoActive)
videoActive = !videoActive
- participantUpdate(sessionData.wrapper, { videoActive })
- signalUserUpdate()
} catch (e) {
console.error(e)
}
@@ -640,6 +651,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
*
@@ -680,6 +750,8 @@
* @param params Connection metadata/params
*/
function publisherCreate(params) {
+ const isSelf = session.connection.connectionId == params.connectionId
+
// Create the element
let wrapper = $(
'<div class="meet-video">'
@@ -699,7 +771,7 @@
// Append the nickname widget
wrapper.find('.controls').before(nicknameWidget(params))
- if (!params.self) {
+ if (!isSelf) {
// Enable audio mute button
wrapper.find('.link-audio').removeClass('hidden')
.on('click', e => {
@@ -731,12 +803,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[isSelf ? 'prependTo' : 'appendTo'](container)
+ .data('cid', params.connectionId)
+ .get(0)
}
/**
@@ -746,27 +818,53 @@
* @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()
+ const wrapper = participantCreate(params)
+ resize()
+ return wrapper;
+ }
+
+ 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
}
/**
@@ -775,13 +873,16 @@
* @param params Connection metadata/params
*/
function subscriberCreate(params) {
+ const isSelf = session.connection.connectionId == params.connectionId
+
// Create the element
let wrapper = $('<div class="meet-subscriber">').append(nicknameWidget(params))
participantUpdate(wrapper, params)
- return wrapper[params.self ? 'prependTo' : 'appendTo'](subscribersContainer)
- .attr('id', 'subscriber-' + params.connId)
+ return wrapper[isSelf ? 'prependTo' : 'appendTo'](subscribersContainer)
+ .attr('id', 'subscriber-' + params.connectionId)
+ .data('cid', params.connectionId)
.get(0)
}
@@ -791,23 +892,40 @@
* @param object params Connection metadata/params
*/
function nicknameWidget(params) {
+ const isSelf = session.connection.connectionId == params.connectionId
+
// 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">CAN_HAZ_AUDIO_AND_VIDEO</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">CAN_MODERATE</span>'
+ //+ '</label>'
+ + '</div>'
+ '</div>'
+ '</div>'
)
let nickname = element.find('.meet-nickname')
- .addClass('btn btn-outline-' + (params.self ? 'primary' : 'secondary'))
+ .addClass('btn btn-outline-' + (isSelf ? 'primary' : 'secondary'))
+ .attr({title: 'Options', 'data-toggle': 'dropdown'})
+ .dropdown({boundary: container})
- if (params.self) {
+ if (isSelf) {
// Add events for nickname change
let editable = element.find('.content')[0]
let editableEnable = () => {
@@ -821,7 +939,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 +950,53 @@
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
+ if (role & Roles.SUBSCRIBER) {
+ role ^= Roles.SUBSCRIBER
+ }
+ } 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 +1019,7 @@
* Update the room "matrix" layout
*/
function updateLayout() {
+ let numOfVideos = $(container).find('.meet-video').length
if (!numOfVideos) {
return
}
@@ -1071,7 +1227,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,89 @@
$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);
+
+ // 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_AND_VIDEO')
+ ->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_AND_VIDEO')
+ ->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);
+
+ // TODO: Demoting the room owner?
+ });
+ }
}
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 }))"
+ );
}
/**
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 10:46 PM (5 h, 49 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831466
Default Alt Text
D2149.1775342774.diff (37 KB)
Attached To
Mode
D2149: Publisher-to-subscriber and vice-versa
Attached
Detach File
Event Timeline