'
)
- 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 = $('
').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 = $(
+ '
'
+ )
+
+ 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