Page MenuHomePhorge

D1984.1774858376.diff
No OneTemporary

Authored By
Unknown
Size
49 KB
Referenced Files
None
Subscribers
None

D1984.1774858376.diff

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 = $('<div id="meet-subscribers">').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 <video> element wrapper with controls
*
- * @param container The parent element
- * @param params Connection metadata/params
+ * @param params Connection metadata/params
*/
- function videoWrapperCreate(container, params) {
+ function publisherCreate(params) {
// Create the element
- let wrapper = $('<div class="meet-video">').html(
- svgIcon('user', 'fas', 'watermark')
- + '<div class="dropdown">'
- + '<a href="#" class="nickname btn btn-link" title="Nickname" 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-dismiss" href="#">Dismiss</a>'
- + '</div>'
- + '</div>'
+ let wrapper = $(
+ '<div class="meet-video">'
+ + svgIcon('user', 'fas', 'watermark')
+ '<div class="controls">'
+ '<button type="button" class="btn btn-link link-audio hidden" title="Mute audio">' + svgIcon('volume-mute') + '</button>'
+ '<button type="button" class="btn btn-link link-fullscreen closed hidden" title="Full screen">' + svgIcon('expand') + '</button>'
@@ -676,56 +689,23 @@
+ '<span class="bg-danger status-audio hidden">' + svgIcon('microphone') + '</span>'
+ '<span class="bg-danger status-video hidden">' + svgIcon('video') + '</span>'
+ '</div>'
+ + '</div>'
)
- if (params.publisher) {
- // Add events for nickname change
- let nickname = wrapper.addClass('publisher').find('.nickname')
- let editable = nickname.find('.content')[0]
- let editableEnable = () => {
- editable.contentEditable = true
- editable.focus()
- }
- let editableUpdate = () => {
- editable.contentEditable = false
- sessionData.params.nickname = editable.innerText
- signalUserUpdate()
- nicknameUpdate(editable.innerText, session.connection.connectionId)
- }
-
- nickname.on('click', editableEnable)
+ // Append the nickname widget
+ wrapper.find('.controls').before(nicknameWidget(params))
- $(editable).on('blur', editableUpdate)
- .on('click', editableEnable)
- .on('keydown', e => {
- // Enter or Esc
- if (e.keyCode == 13 || e.keyCode == 27) {
- editableUpdate()
- return false
- }
- })
- } else {
+ if (!params.self) {
+ // Enable audio mute button
wrapper.find('.link-audio').removeClass('hidden')
.on('click', e => {
let video = wrapper.find('video')[0]
video.muted = !video.muted
wrapper.find('.link-audio')[video.muted ? 'addClass' : 'removeClass']('text-danger')
})
-
- if (role == 'MODERATOR') {
- wrapper.addClass('moderated')
-
- wrapper.find('.nickname').attr({title: 'Options', 'data-toggle': 'dropdown'}).dropdown()
-
- wrapper.find('.action-dismiss').on('click', e => {
- if (sessionData.onDismiss) {
- sessionData.onDismiss(params.connId)
- }
- })
- }
}
- videoWrapperUpdate(wrapper, params)
+ participantUpdate(wrapper, params)
// Fullscreen control
if (document.fullscreenEnabled) {
@@ -749,7 +729,10 @@
numOfVideos++
- return wrapper[params.publisher ? 'prependTo' : 'appendTo'](container).get(0)
+ // Remove the subscriber element, if exists
+ $('#subscriber-' + params.connId).remove()
+
+ return wrapper[params.self ? 'prependTo' : 'appendTo'](container).get(0)
}
/**
@@ -758,18 +741,104 @@
* @param wrapper The wrapper element
* @param params Connection metadata/params
*/
- function videoWrapperUpdate(wrapper, params) {
+ function participantUpdate(wrapper, params) {
+ const $element = $(wrapper)
+
if ('audioActive' in params) {
- $(wrapper).find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden')
+ $element.find('.status-audio')[params.audioActive ? 'addClass' : 'removeClass']('hidden')
}
if ('videoActive' in params) {
- $(wrapper).find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden')
+ $element.find('.status-video')[params.videoActive ? 'addClass' : 'removeClass']('hidden')
}
if ('nickname' in params) {
- $(wrapper).find('.nickname > .content').text(params.nickname)
+ $element.find('.meet-nickname > .content').text(params.nickname)
+ }
+
+ if (params.self) {
+ $element.addClass('self')
}
+
+ if (role == 'MODERATOR') {
+ $element.addClass('moderated')
+ }
+ }
+
+ /**
+ * Create a tag-like element for a subscriber participant
+ *
+ * @param params Connection metadata/params
+ */
+ function subscriberCreate(params) {
+ // 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)
+ .get(0)
+ }
+
+ /**
+ * Create a tag-like nickname widget
+ *
+ * @param object params Connection metadata/params
+ */
+ function nicknameWidget(params) {
+ // Create the element
+ let element = $(
+ '<div class="dropdown">'
+ + '<a href="#" class="meet-nickname btn" title="Nickname" 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-dismiss" href="#">Dismiss</a>'
+ + '</div>'
+ + '</div>'
+ )
+
+ let nickname = element.find('.meet-nickname')
+ .addClass('btn btn-outline-' + (params.self ? 'primary' : 'secondary'))
+
+ if (params.self) {
+ // Add events for nickname change
+ let editable = element.find('.content')[0]
+ let editableEnable = () => {
+ editable.contentEditable = true
+ editable.focus()
+ }
+ let editableUpdate = () => {
+ editable.contentEditable = false
+ sessionData.params.nickname = editable.innerText
+ signalUserUpdate()
+ nicknameUpdate(editable.innerText, session.connection.connectionId)
+ }
+
+ nickname.on('click', editableEnable)
+
+ $(editable).on('blur', editableUpdate)
+ .on('keydown', e => {
+ // Enter or Esc
+ if (e.keyCode == 13 || e.keyCode == 27) {
+ editableUpdate()
+ return false
+ }
+ })
+ } else if (role == 'MODERATOR') {
+ nickname.attr({title: 'Options', 'data-toggle': 'dropdown'})
+ .dropdown({boundary: container})
+
+ element.find('.action-dismiss').on('click', e => {
+ if (sessionData.onDismiss) {
+ sessionData.onDismiss(params.connId)
+ }
+ })
+ }
+
+ return element.get(0)
}
/**
@@ -778,6 +847,11 @@
function resize() {
containerWidth = container.offsetWidth
containerHeight = container.offsetHeight
+
+ if (subscribersContainer) {
+ containerHeight -= subscribersContainer.offsetHeight
+ }
+
updateLayout()
$(container).parent()[window.screen.width <= 768 ? 'addClass' : 'removeClass']('mobile')
}
@@ -986,6 +1060,15 @@
volumeElement.firstChild.style.height = 0
}
}
+
+ function connectionData(connection) {
+ // Note: we're sending a json from two sources (server-side when
+ // creating a token/connection, and client-side when joining the session)
+ // 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('}%/%{', ','))
+ }
}
export default Meet
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
@@ -1,3 +1,42 @@
+.meet-nickname {
+ padding: 0;
+ line-height: 2em;
+ border-radius: 1em;
+ max-width: 100%;
+ white-space: nowrap;
+ display: inline-flex;
+
+ .icon {
+ display: inline-block;
+ min-width: 2em;
+ }
+
+ .content {
+ order: 1;
+ height: 2em;
+ outline: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:not(:empty) {
+ margin-left: 0.5em;
+ padding-right: 0.5em;
+
+ & + .icon {
+ margin-right: -0.75em;
+ }
+ }
+ }
+
+ .self & {
+ .content {
+ &:focus {
+ min-width: 0.5em;
+ }
+ }
+ }
+}
+
.meet-video {
position: relative;
background: $menu-bg-color;
@@ -33,13 +72,6 @@
height: 50%;
}
- .dropdown {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- }
-
.controls {
position: absolute;
bottom: 0;
@@ -75,64 +107,26 @@
}
}
- .nickname {
- position: absolute;
+ .dropdown {
+ position: absolute !important;
top: 0;
left: 0;
+ right: 0;
+ }
+
+ .meet-nickname {
margin: 0.5em;
- padding: 0 1em;
- line-height: 2em;
- border-radius: 1em;
max-width: calc(100% - 1em);
- background: rgba(#fff, 0.8);
- color: $body-color;
- text-decoration: none !important;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- display: flex;
+ border: 0;
- .icon {
- display: none;
- width: 2em;
- margin: 0 -1em;
- }
-
- .content {
- outline: none;
+ &:not(:hover) {
+ background-color: rgba(#fff, 0.8);
}
}
- &.moderated .nickname {
- display: flex;
-
- .content {
- order: 1;
-
- &:not(:empty) + .icon {
- margin-right: 0;
- }
- }
-
+ &:not(.moderated):not(.self) .meet-nickname {
.icon {
- display: inline-block;
- }
- }
-
- &.publisher .nickname {
- background: rgba($main-color, 0.9);
-
- &:focus-within {
- box-shadow: $btn-focus-box-shadow;
- }
-
- .content:empty {
- display: block;
- height: 2em;
-
- &:not(:focus) + .icon {
- display: inline-block;
- }
+ display: none;
}
}
}
@@ -203,7 +197,36 @@
justify-content: center;
flex-wrap: wrap;
flex: 1;
- //overflow: hidden;
+ position: relative; // for #meet-subscribers positioning
+}
+
+#meet-subscribers {
+ display: flex;
+ flex-wrap: wrap;
+ order: 999;
+ padding: 0.15em;
+ width: 100%;
+ overflow-y: auto;
+
+ &:empty {
+ display: none;
+ }
+
+ .meet-subscriber {
+ margin: 0.15em;
+ max-width: calc(25% - 0.4em);
+ }
+
+ // when the subscribers list is the only child this means
+ // there's no publisher videos in the room yet
+ &:only-child {
+ justify-content: center;
+ align-content: center;
+ }
+
+ &:not(:only-child) {
+ max-height: 30%;
+ }
}
#meet-chat {
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
@@ -205,14 +205,13 @@
init: init ? 1 : 0,
picture: init ? this.makePicture() : '',
requestId: this.requestId(),
- role: this.camera || this.microphone ? 'PUBLISHER' : 'SUBSCRIBER'
+ canPublish: !!this.camera || !!this.microphone
}
$('#setup-password,#setup-nickname').removeClass('is-invalid')
axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
.then(response => {
- // Response data contains: session, token and shareToken
this.roomState = 'ready'
this.session = response.data
@@ -221,6 +220,11 @@
}
})
.catch(error => {
+ if (!error.response) {
+ console.error(error)
+ return
+ }
+
const data = error.response.data || {}
if (data.code) {
@@ -267,7 +271,7 @@
}
},
isPublisher() {
- return this.session && this.session.role && this.session.role != 'SUBSCRIBER'
+ return this.session && this.session.canPublish
},
isRoomReady() {
return ['ready', 322, 324, 325, 326, 327].includes(this.roomState)
diff --git a/src/resources/vue/Widgets/StatusMessage.vue b/src/resources/vue/Widgets/StatusMessage.vue
--- a/src/resources/vue/Widgets/StatusMessage.vue
+++ b/src/resources/vue/Widgets/StatusMessage.vue
@@ -18,8 +18,8 @@
export default {
props: {
- status: { type: String, default: () => 'init' },
- statusLabels: { type: Object, default: () => defaultLabels }
+ status: { type: [String, Number], default: 'init' },
+ statusLabels: { type: Object, default: defaultLabels }
},
methods: {
statusClass() {
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
@@ -116,7 +116,7 @@
->assertSeeIn('@setup-button', "JOIN")
// Join the room, disable cam/mic
->select('@setup-mic-select', '')
- ->select('@setup-cam-select', '')
+ //->select('@setup-cam-select', '')
->click('@setup-button')
->waitFor('@session');
@@ -130,48 +130,47 @@
'security' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
])
- ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ ->whenAvailable('div.meet-video.self', function (Browser $browser) {
$browser->assertVisible('video')
->assertAudioMuted('video', true)
- ->assertSeeIn('.nickname', 'john')
+ ->assertSeeIn('.meet-nickname', 'john')
->assertVisible('.controls button.link-fullscreen')
->assertMissing('.controls button.link-audio')
->assertMissing('.status .status-audio')
->assertMissing('.status .status-video');
})
- ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
- $browser->assertMissing('video')
- ->assertVisible('.nickname')
+ ->whenAvailable('div.meet-video:not(.self)', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertVisible('.meet-nickname')
->assertVisible('.controls button.link-fullscreen')
->assertVisible('.controls button.link-audio')
->assertVisible('.status .status-audio')
- ->assertVisible('.status .status-video');
+ ->assertMissing('.status .status-video');
})
->assertElementsCount('@session div.meet-video', 2);
// Assert current UI state
$guest->assertToolbar([
- 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
- 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
- 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
+ 'video' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
])
- ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ ->whenAvailable('div.meet-video:not(.self)', function (Browser $browser) {
$browser->assertVisible('video')
- //->assertAudioMuted('video', true)
+ ->assertSeeIn('.meet-nickname', 'john')
->assertVisible('.controls button.link-fullscreen')
- ->assertMissing('.controls button.link-audio')
- ->assertVisible('.status .status-audio')
- ->assertVisible('.status .status-video');
+ ->assertVisible('.controls button.link-audio')
+ ->assertMissing('.status .status-audio')
+ ->assertMissing('.status .status-video');
})
- ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ ->whenAvailable('div.meet-video.self', function (Browser $browser) {
$browser->assertVisible('video')
- ->assertSeeIn('.nickname', 'john')
->assertVisible('.controls button.link-fullscreen')
- ->assertVisible('.controls button.link-audio')
- ->assertMissing('.status .status-audio')
+ ->assertMissing('.controls button.link-audio')
+ ->assertVisible('.status .status-audio')
->assertMissing('.status .status-video');
})
->assertElementsCount('@session div.meet-video', 2);
@@ -179,44 +178,44 @@
// Test nickname change propagation
// Use script() because type() does not work with this contenteditable widget
- $guest->setNickname('div.meet-video.publisher', 'guest');
- $owner->waitFor('div.meet-video:not(.publisher) .nickname')
- ->assertSeeIn('div.meet-video:not(.publisher) .nickname', 'guest');
+ $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');
// Test muting audio
$owner->click('@menu button.link-audio')
->assertToolbarButtonState('audio', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
- ->assertVisible('div.meet-video.publisher .status .status-audio');
+ ->assertVisible('div.meet-video.self .status .status-audio');
// FIXME: It looks that we can't just check the <video> element state
// We might consider using OpenVidu API to make sure
- $guest->waitFor('div.meet-video:not(.publisher) .status .status-audio');
+ $guest->waitFor('div.meet-video:not(.self) .status .status-audio');
// Test unmuting audio
$owner->click('@menu button.link-audio')
->assertToolbarButtonState('audio', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
- ->assertMissing('div.meet-video.publisher .status .status-audio');
+ ->assertMissing('div.meet-video.self .status .status-audio');
- $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-audio');
+ $guest->waitUntilMissing('div.meet-video:not(.self) .status .status-audio');
// Test muting video
$owner->click('@menu button.link-video')
->assertToolbarButtonState('video', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
- ->assertVisible('div.meet-video.publisher .status .status-video');
+ ->assertVisible('div.meet-video.self .status .status-video');
// FIXME: It looks that we can't just check the <video> element state
// We might consider using OpenVidu API to make sure
- $guest->waitFor('div.meet-video:not(.publisher) .status .status-video');
+ $guest->waitFor('div.meet-video:not(.self) .status .status-video');
// Test unmuting video
$owner->click('@menu button.link-video')
->assertToolbarButtonState('video', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
- ->assertMissing('div.meet-video.publisher .status .status-video');
+ ->assertMissing('div.meet-video.self .status .status-video');
- $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-video');
+ $guest->waitUntilMissing('div.meet-video:not(.self) .status .status-video');
// Test muting other user
- $guest->with('div.meet-video:not(.publisher)', function (Browser $browser) {
+ $guest->with('div.meet-video:not(.self)', function (Browser $browser) {
$browser->click('.controls button.link-audio')
->assertAudioMuted('video', true)
->assertVisible('.controls button.link-audio.text-danger')
@@ -254,7 +253,7 @@
->assertSeeIn('@setup-button', "JOIN")
// Join the room, disable cam/mic
->select('@setup-mic-select', '')
- ->select('@setup-cam-select', '')
+ // ->select('@setup-cam-select', '')
->click('@setup-button')
->waitFor('@session');
@@ -310,7 +309,7 @@
// Test nickname change is propagated to chat messages
- $guest->setNickname('div.meet-video.publisher', 'guest')
+ $guest->setNickname('div.meet-video.self', 'guest')
->keys('@chat-input', 'guest2', '{enter}')
->assertElementsCount('@chat-list .message', 2)
->assertSeeIn('@chat-list .message:last-child .nickname', 'guest')
diff --git a/src/tests/Browser/Meet/RoomSecurityTest.php b/src/tests/Browser/Meet/RoomSecurityTest.php
--- a/src/tests/Browser/Meet/RoomSecurityTest.php
+++ b/src/tests/Browser/Meet/RoomSecurityTest.php
@@ -221,20 +221,20 @@
// Guest automatically anters the room
$guest->waitFor('@session', 12)
// make sure he has no access to the Options menu
- ->waitFor('@session .meet-video:not(.publisher)')
- ->assertSeeIn('@session .meet-video:not(.publisher) a.nickname', 'John')
+ ->waitFor('@session .meet-video:not(.self)')
+ ->assertSeeIn('@session .meet-video:not(.self) .meet-nickname', 'John')
// TODO: Assert title and icon
- ->click('@session .meet-video:not(.publisher) a.nickname')
+ ->click('@session .meet-video:not(.self) .meet-nickname')
->pause(100)
->assertMissing('.dropdown-menu');
// Test dismissing the participant
- $owner->click('@session .meet-video:not(.publisher) a.nickname')
- ->waitFor('@session .meet-video:not(.publisher) .dropdown-menu')
- ->assertSeeIn('@session .meet-video:not(.publisher) .dropdown-menu > .action-dismiss', 'Dismiss')
- ->click('@session .meet-video:not(.publisher) .dropdown-menu > .action-dismiss')
+ $owner->click('@session .meet-video:not(.self) .meet-nickname')
+ ->waitFor('@session .meet-video:not(.self) .dropdown-menu')
+ ->assertSeeIn('@session .meet-video:not(.self) .dropdown-menu > .action-dismiss', 'Dismiss')
+ ->click('@session .meet-video:not(.self) .dropdown-menu > .action-dismiss')
->waitUntilMissing('.dropdown-menu')
- ->waitUntilMissing('@session .meet-video:not(.publisher)');
+ ->waitUntilMissing('@session .meet-video:not(.self)');
// Expect a "end of session" dialog on the participant side
$guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
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
@@ -170,9 +170,9 @@
->click('@setup-button')
->waitFor('@session')
->assertMissing('@setup-form')
- ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ ->whenAvailable('div.meet-video.self', function (Browser $browser) {
$browser->assertVisible('video')
- ->assertSeeIn('.nickname', 'john')
+ ->assertSeeIn('.meet-nickname', 'john')
->assertVisible('.controls button.link-fullscreen')
->assertMissing('.controls button.link-audio')
->assertMissing('.status .status-audio')
@@ -193,21 +193,21 @@
->assertSeeIn('@setup-button', "JOIN")
// Join the room, disable cam/mic
->select('@setup-mic-select', '')
- ->select('@setup-cam-select', '')
+ //->select('@setup-cam-select', '')
->click('@setup-button')
->waitFor('@session')
->assertMissing('@setup-form')
- ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
+ ->whenAvailable('div.meet-video.self', function (Browser $browser) {
$browser->assertVisible('video')
- ->assertVisible('.nickname')
+ ->assertVisible('.meet-nickname')
->assertVisible('.controls button.link-fullscreen')
->assertMissing('.controls button.link-audio')
->assertVisible('.status .status-audio')
- ->assertVisible('.status .status-video');
+ ->assertMissing('.status .status-video');
})
- ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
+ ->whenAvailable('div.meet-video:not(.self)', function (Browser $browser) {
$browser->assertVisible('video')
- ->assertSeeIn('.nickname', 'john')
+ ->assertSeeIn('.meet-nickname', 'john')
->assertVisible('.controls button.link-fullscreen')
->assertVisible('.controls button.link-audio')
->assertMissing('.status .status-audio')
@@ -225,16 +225,16 @@
}
// Check guest's elements in the owner's window
- $browser->waitFor('@session div.meet-video:nth-child(2)')
- ->assertElementsCount('@session div.meet-video', 2)
- ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
- $browser->assertMissing('video')
- ->assertVisible('.nickname')
+ $browser
+ ->whenAvailable('div.meet-video:not(.self)', function (Browser $browser) {
+ $browser->assertVisible('video')
+ ->assertVisible('.meet-nickname')
->assertVisible('.controls button.link-fullscreen')
->assertVisible('.controls button.link-audio')
->assertVisible('.status .status-audio')
- ->assertVisible('.status .status-video');
- });
+ ->assertMissing('.status .status-video');
+ })
+ ->assertElementsCount('@session div.meet-video', 2);
// Test leaving the room
@@ -243,7 +243,7 @@
->waitForLocation('/login');
// Expect the participant removed from other users windows
- $browser->waitUntilMissing('@session div.meet-video:nth-child(2)');
+ $browser->waitUntilMissing('@session div.meet-video:not(.self)');
// Join the room as guest again
$guest->visit(new RoomPage('john'))
@@ -258,7 +258,7 @@
->assertSeeIn('@setup-button', "JOIN")
// Join the room, disable cam/mic
->select('@setup-mic-select', '')
- ->select('@setup-cam-select', '')
+ //->select('@setup-cam-select', '')
->click('@setup-button')
->waitFor('@session');
@@ -280,4 +280,91 @@
->waitForLocation('/login');
});
}
+
+ /**
+ * Test two subscribers-only users in a room
+ *
+ * @group openvidu
+ * @depends testTwoUsersInARoom
+ */
+ public function testSubscribers(): 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')
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->whenAvailable('@subscribers .meet-subscriber.self', function (Browser $browser) {
+ $browser->assertSeeIn('.meet-nickname', 'john');
+ })
+ ->assertElementsCount('@session div.meet-video', 0)
+ ->assertElementsCount('@session video', 0)
+ ->assertElementsCount('@session .meet-subscriber', 1)
+ ->assertToolbar([
+ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
+ 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'security' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ ]);
+
+ // After the owner "opened the room" guest should be able to join
+ // In one browser window act as a guest
+ $guest->visit(new RoomPage('john'))
+ ->waitUntilMissing('@setup-status-message', 10)
+ ->assertSeeIn('@setup-button', "JOIN")
+ // Join the room, disable cam/mic
+ ->select('@setup-mic-select', '')
+ ->select('@setup-cam-select', '')
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->whenAvailable('@subscribers .meet-subscriber.self', function (Browser $browser) {
+ $browser->assertVisible('.meet-nickname');
+ })
+ ->whenAvailable('@subscribers .meet-subscriber:not(.self)', function (Browser $browser) {
+ $browser->assertSeeIn('.meet-nickname', 'john');
+ })
+ ->assertElementsCount('@session div.meet-video', 0)
+ ->assertElementsCount('@session video', 0)
+ ->assertElementsCount('@session div.meet-subscriber', 2)
+ ->assertToolbar([
+ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+ 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
+ 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+ ]);
+
+ // Check guest's elements in the owner's window
+ $browser
+ ->whenAvailable('@subscribers .meet-subscriber:not(.self)', function (Browser $browser) {
+ $browser->assertVisible('.meet-nickname');
+ })
+ ->assertElementsCount('@session div.meet-video', 0)
+ ->assertElementsCount('@session video', 0)
+ ->assertElementsCount('@session .meet-subscriber', 2);
+
+ // Test leaving the room
+
+ // Guest is leaving
+ $guest->click('@menu button.link-logout')
+ ->waitForLocation('/login');
+
+ // Expect the participant removed from other users windows
+ $browser->waitUntilMissing('@session .meet-subscriber:not(.self)');
+ });
+ }
}
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
@@ -75,6 +75,7 @@
'@menu' => '#meet-session-menu',
'@session' => '#meet-session',
+ '@subscribers' => '#meet-subscribers',
'@chat' => '#meet-chat',
'@chat-input' => '#meet-chat textarea',
@@ -161,7 +162,7 @@
public function setNickname($browser, $selector, $nickname): void
{
// Use script() because type() does not work with this contenteditable widget
- $selector = $selector . ' .nickname span';
+ $selector = $selector . ' .meet-nickname .content';
$browser->script(
"var element = document.querySelector('$selector');"
. "element.focus();"
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
@@ -116,6 +116,7 @@
$this->assertSame(Room::ROLE_MODERATOR, $json['role']);
$this->assertSame($session_id, $json['session']);
+ $this->assertFalse($json['canPublish']);
$this->assertTrue(is_string($session_id) && !empty($session_id));
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue(!array_key_exists('shareToken', $json));
@@ -132,20 +133,21 @@
$this->assertTrue(empty($json['token']));
$this->assertTrue(empty($json['shareToken']));
- // Non-owner, now the session exists, with 'init', but no 'role' argument
+ // Non-owner, now the session exists, with 'init', but no 'canPublish' argument
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(Room::ROLE_SUBSCRIBER, $json['role']);
+ $this->assertFalse($json['canPublish']);
$this->assertSame($session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue($json['token'] != $john_token);
$this->assertTrue(empty($json['shareToken']));
// Non-owner, now the session exists, with 'init', and with 'role=PUBLISHER'
- $post = ['role' => Room::ROLE_PUBLISHER, 'init' => 1];
+ $post = ['canPublish' => true, 'init' => 1];
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
$response->assertStatus(200);
@@ -153,6 +155,7 @@
$this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
$this->assertSame($session_id, $json['session']);
+ $this->assertTrue($json['canPublish']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue($json['token'] != $john_token);
$this->assertTrue(!array_key_exists('shareToken', $json));
@@ -322,12 +325,13 @@
// Non-owner, locked room, join request accepted
$post['init'] = 1;
- $post['role'] = Room::ROLE_PUBLISHER;
+ $post['canPublish'] = true;
$response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
+ $this->assertTrue($json['canPublish']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
// TODO: Test a scenario where both password and lock are enabled
@@ -348,13 +352,14 @@
$room = Room::where('name', 'john')->first();
// Guest, request with screenShare token
- $post = ['role' => Room::ROLE_PUBLISHER, 'screenShare' => 1, 'init' => 1];
+ $post = ['canPublish' => true, 'screenShare' => 1, 'init' => 1];
$response = $this->post("api/v4/openvidu/rooms/{$room->name}", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertSame(Room::ROLE_PUBLISHER, $json['role']);
+ $this->assertTrue($json['canPublish']);
$this->assertSame($room->session_id, $json['session']);
$this->assertTrue(strpos($json['token'], 'wss://') === 0);
$this->assertTrue(strpos($json['shareToken'], 'wss://') === 0);
diff --git a/src/tests/TestCaseDusk.php b/src/tests/TestCaseDusk.php
--- a/src/tests/TestCaseDusk.php
+++ b/src/tests/TestCaseDusk.php
@@ -34,6 +34,7 @@
'--disable-gpu',
'--headless',
'--use-fake-ui-for-media-stream',
+ '--use-fake-device-for-media-stream',
'--ignore-certificate-errors',
'--incognito',
]);

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 30, 8:12 AM (3 d, 9 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18798297
Default Alt Text
D1984.1774858376.diff (49 KB)

Event Timeline