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 @@ -41,6 +41,7 @@ let containerHeight let chatCount = 0 let volumeElement + let publishersContainer let subscribersContainer let scrollStop @@ -107,6 +108,10 @@ * onMediaSetup - Called when user clicks the Media setup button */ function joinRoom(data) { + // Create a container for subscribers and publishers + publishersContainer = $('
').appendTo(container).get(0) + subscribersContainer = $('
').appendTo(container).get(0) + resize(); volumeMeterStop() @@ -115,11 +120,6 @@ // avatar: undefined // avatar image } - // Create a container for subscribers - if (!subscribersContainer) { - subscribersContainer = $('
').appendTo(container).get(0) - } - // TODO: Make sure all supported callbacks exist, so we don't have to check // their existence everywhere anymore @@ -185,6 +185,12 @@ // Subscribe to the Stream to receive it let subscriber = session.subscribe(event.stream, metadata.element, props); + Object.assign(metadata, { + audioActive: event.stream.audioActive, + videoActive: event.stream.videoActive, + videoDimensions: event.stream.videoDimensions + }) + subscriber.on('videoElementCreated', event => { $(event.element).prop({ tabindex: -1 @@ -193,9 +199,6 @@ resize() }) - metadata.audioActive = event.stream.audioActive - metadata.videoActive = event.stream.videoActive - // Update the wrapper controls/status participantUpdate(metadata.element, metadata) }) @@ -213,7 +216,12 @@ if (metadata) { metadata[event.changedProperty] = event.newValue - participantUpdate(metadata.element, metadata) + + if (event.changedProperty == 'videoDimensions') { + resize() + } else { + participantUpdate(metadata.element, metadata) + } } }) @@ -991,6 +999,8 @@ * @param content Optional content to prepend to the element */ function publisherCreate(params, content) { + let isScreen = params.role & Roles.SCREEN + // Create the element let wrapper = $( '
' @@ -1015,6 +1025,10 @@ wrapper.prepend(content) } + if (isScreen) { + wrapper.addClass('screen') + } + if (params.isSelf) { if (sessionData.onMediaSetup) { wrapper.find('.link-setup').removeClass('hidden') @@ -1055,7 +1069,9 @@ // Remove the subscriber element, if exists $('#subscriber-' + params.connectionId).remove() - return wrapper[params.isSelf ? 'prependTo' : 'appendTo'](container) + let prio = params.isSelf || (isScreen && !$(publishersContainer).children('.screen').length) + + return wrapper[prio ? 'prependTo' : 'appendTo'](publishersContainer) .attr('id', 'publisher-' + params.connectionId) .get(0) } @@ -1357,12 +1373,8 @@ * Window onresize event handler (updates room layout) */ function resize() { - containerWidth = container.offsetWidth - containerHeight = container.offsetHeight - - if (subscribersContainer) { - containerHeight -= subscribersContainer.offsetHeight - } + containerWidth = publishersContainer.offsetWidth + containerHeight = publishersContainer.offsetHeight updateLayout() $(container).parent()[window.screen.width <= 768 ? 'addClass' : 'removeClass']('mobile') @@ -1372,12 +1384,59 @@ * Update the room "matrix" layout */ function updateLayout() { - let numOfVideos = $(container).find('.meet-video').length + let publishers = $(publishersContainer).find('.meet-video') + let numOfVideos = publishers.length + if (!numOfVideos) { return } - let css, rows, cols, height + let css, rows, cols, height, padding = 0 + + // Make the first screen sharing tile big + let screenVideo = publishers.filter('.screen').find('video').get(0) + + if (screenVideo) { + let element = screenVideo.parentNode + let connId = element.id.replace(/^publisher-/, '') + let connection = connections[connId] + + // We know the shared screen video dimensions, we can calculate + // width/height of the tile in the matrix + if (connection && connection.videoDimensions) { + let screenWidth = connection.videoDimensions.width + let screenHeight = containerHeight + + // TODO: When the shared window is minimized the width/height is set to 1 (or 2) + // - at least on my system. We might need to handle this case nicer. Right now + // it create a 1-2px line on the left of the matrix - not a big issue. + // TODO: Make the 0.666 factor bigger for wide screen and small number of participants? + let maxWidth = Math.ceil(containerWidth * 0.666) + + if (screenWidth > maxWidth) { + screenWidth = maxWidth + } + + // Set the tile position and size + $(element).css({ + width: screenWidth + 'px', + height: screenHeight + 'px', + position: 'absolute', + top: 0, + left: 0 + }) + + padding = screenWidth + 'px' + + // Now the estate for the rest of participants is what's left on the right side + containerWidth -= screenWidth + publishers = publishers.not(element) + numOfVideos -= 1 + } + } + + // Compensate the shared screen estate with a padding + $(publishersContainer).css('padding-left', padding) const factor = containerWidth / containerHeight @@ -1420,27 +1479,12 @@ // console.log('factor=' + factor, 'num=' + numOfVideos, 'cols = '+cols, 'rows=' + rows); - height = containerHeight / rows - css = { - width: (100 / cols) + '%', + // Update all tiles (except the main shared screen) in the matrix + publishers.css({ + width: (containerWidth / cols) + 'px', // Height must be in pixels to make object-fit:cover working - height: height + 'px' - } - - // Update the matrix - $(container).find('.meet-video').css(css) - /* - .each((idx, elem) => { - let video = $(elem).children('video')[0] - - if (video && video.videoWidth && video.videoHeight && video.videoWidth > video.videoHeight) { - // Set max-width to keep the original aspect ratio in cases - // when there's enough room to display the element - let maxWidth = height * video.videoWidth / video.videoHeight - $(elem).css('max-width', maxWidth) - } - }) - */ + height: (containerHeight / rows) + 'px' + }) } /** 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 @@ -65,6 +65,12 @@ } } + &.screen video { + // For shared screen videos we use the original aspect ratio + object-fit: scale-down; + background: none; + } + &.fullscreen { video { // We don't want the video to be cut in fullscreen @@ -195,42 +201,20 @@ overflow: hidden; } -#meet-session { - display: flex; - justify-content: center; - flex-wrap: wrap; - flex: 1; - position: relative; // for #meet-subscribers positioning +#meet-publishers { + height: 100%; + position: relative; } #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%; - } - // Language interpreters will be displayed as subscribers, but will have still // the video element that we will hide video { @@ -238,6 +222,37 @@ } } +#meet-session { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + & > div { + display: flex; + flex-wrap: wrap; + width: 100%; + + &:empty { + display: none; + } + } + + #meet-publishers:empty { + & + #meet-subscribers { + justify-content: center; + align-content: center; + flex: 1; + } + } + + #meet-publishers:not(:empty) { + & + #meet-subscribers { + max-height: 30%; + } + } +} + #meet-chat { width: 0; display: none; 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 @@ -362,7 +362,7 @@ ->assertToolbarButtonState('screen', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED); $guest - ->whenAvailable('div.meet-video:nth-child(3)', function (Browser $browser) { + ->whenAvailable('div.meet-video.screen', function (Browser $browser) { $browser->waitFor('video') ->assertSeeIn('.meet-nickname', 'john') ->assertVisible('.controls button.link-fullscreen') diff --git a/src/tests/Browser/Meet/RoomModeratorTest.php b/src/tests/Browser/Meet/RoomModeratorTest.php --- a/src/tests/Browser/Meet/RoomModeratorTest.php +++ b/src/tests/Browser/Meet/RoomModeratorTest.php @@ -78,7 +78,7 @@ ->assertMissing('@session div.meet-subscriber:not(.self) svg.user') // owner ->assertVisible('@session div.meet-subscriber:not(.self) svg.moderator') // owner ->click('@session div.meet-subscriber.self .meet-nickname') - ->whenAvailable('@session .dropdown-menu', function (Browser $browser) { + ->whenAvailable('@session div.meet-subscriber.self .dropdown-menu', function (Browser $browser) { $browser->assertMissing('.permissions'); }) ->click('@session div.meet-subscriber:not(.self) .meet-nickname') @@ -101,7 +101,7 @@ ->assertMissing('@session div.meet-subscriber.self svg.user') // self ->assertVisible('@session div.meet-subscriber.self svg.moderator') // self ->click('@session div.meet-subscriber.self .meet-nickname') - ->whenAvailable('@session .dropdown-menu', function (Browser $browser) { + ->whenAvailable('@session div.meet-subscriber.self .dropdown-menu', function (Browser $browser) { $browser->assertChecked('.action-role-moderator input') ->assertDisabled('.action-role-moderator input'); })