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