Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117549042
D1984.1774858376.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
49 KB
Referenced Files
None
Subscribers
None
D1984.1774858376.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
@@ -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
Details
Attached
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)
Attached To
Mode
D1984: Separate subscribers list
Attached
Detach File
Event Timeline