Page MenuHomePhorge

D1984.1775275178.diff
No OneTemporary

Authored By
Unknown
Size
23 KB
Referenced Files
None
Subscribers
None

D1984.1775275178.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,16 @@
// 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;
+ if (request()->input('role') === Room::ROLE_PUBLISHER) {
+ $req_role = Room::ROLE_PUBLISHER;
} else {
- $role = Room::ROLE_SUBSCRIBER;
+ $req_role = Room::ROLE_SUBSCRIBER;
}
+ $role = $isOwner ? Room::ROLE_MODERATOR : $req_role;
+
// Create session token for the current user/connection
- $response = $room->getSessionToken($role);
+ $response = $room->getSessionToken($role, ['role' => $req_role]);
if (empty($response)) {
return $this->errorResponse(500, \trans('meet.session-join-error'));
@@ -306,6 +306,7 @@
$response['role'] = $role;
$response['owner'] = $isOwner;
$response['config'] = $config;
+ $response['role'] = $req_role;
} else {
$response_code = 422;
$response['code'] = 322;
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,10 +169,14 @@
/**
* 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");
@@ -181,7 +185,12 @@
$url = 'sessions/' . $this->session_id . '/connection';
$post = [
'json' => [
- 'role' => $role
+ 'role' => $role,
+ // FIXME: Looks like passing the role in 'data' is the only way
+ // to make it visible for everyone in a room. So, for example we can
+ // handle/style subscribers and publishers differently on the client-side.
+ // Is this a security issue?
+ 'data' => !empty($data) ? json_encode($data) : null
]
];
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()
@@ -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)
+ publisherUpdate(wrapper, event.stream)
})
/*
session.on('streamDestroyed', event => {
@@ -169,7 +172,7 @@
data.onDestroy(event)
}
- updateLayout()
+ resize()
})
// Handle signals from all participants
@@ -178,8 +181,8 @@
// 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 = Object.assign({}, data.params, { self: true, audioActive, videoActive })
publisher.on('videoElementCreated', event => {
$(event.element).prop({
@@ -187,17 +190,20 @@
disablePictureInPicture: true, // this does not work in Firefox
tabindex: -1
})
- updateLayout()
+ resize()
})
- publisher.createVideoElement(wrapper, 'PREPEND')
-
- sessionData.wrapper = wrapper
-
- // Publish the stream
- if (sessionData.role != 'SUBSCRIBER') {
+ if (sessionData.role == 'PUBLISHER') {
+ wrapper = participantCreate(params)
+ publisher.createVideoElement(wrapper, 'PREPEND')
session.publish(publisher)
+ } else {
+ wrapper = subscriberCreate(params)
}
+
+ resize()
+
+ sessionData.wrapper = wrapper
})
.catch(error => {
console.error('There was an error connecting to the session: ', error.message);
@@ -427,7 +433,7 @@
if (conn = connections[connId]) {
data = JSON.parse(signal.data)
- videoWrapperUpdate(conn.element, data)
+ publisherUpdate(conn.element, data)
nicknameUpdate(data.nickname, connId)
}
break
@@ -571,7 +577,7 @@
try {
publisher.publishAudio(!audioActive)
audioActive = !audioActive
- videoWrapperUpdate(sessionData.wrapper, { audioActive })
+ publisherUpdate(sessionData.wrapper, { audioActive })
signalUserUpdate()
} catch (e) {
console.error(e)
@@ -593,7 +599,7 @@
try {
publisher.publishVideo(!videoActive)
videoActive = !videoActive
- videoWrapperUpdate(sessionData.wrapper, { videoActive })
+ publisherUpdate(sessionData.wrapper, { videoActive })
signalUserUpdate()
} catch (e) {
console.error(e)
@@ -648,25 +654,33 @@
}
}
+ /**
+ * Create a participant element in the matrix. Depending on the role
+ * 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.role == 'SUBSCRIBER') {
+ return subscriberCreate(params)
+ }
+
+ return publisherCreate(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 +690,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)
+ publisherUpdate(wrapper, params)
// Fullscreen control
if (document.fullscreenEnabled) {
@@ -749,7 +730,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,26 +742,117 @@
* @param wrapper The wrapper element
* @param params Connection metadata/params
*/
- function videoWrapperUpdate(wrapper, params) {
+ function publisherUpdate(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('publisher') // TODO: change class name to '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>').append(nicknameWidget(params))
+
+ publisherUpdate(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)
+ }
+
/**
* Window onresize event handler (updates room layout)
*/
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 +1061,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;
+ }
+ }
+ }
+
+ .publisher & {
+ .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(.publisher) .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;
+ }
+
+ & > div {
+ 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
@@ -212,7 +212,6 @@
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) {
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/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',

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 3:59 AM (18 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828003
Default Alt Text
D1984.1775275178.diff (23 KB)

Event Timeline