diff --git a/resources/css/app.css b/resources/css/app.css index 81a0815..f2d92d3 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -62,16 +62,28 @@ html.meet-room-page { margin-left: auto; } -.meet-room-page .meet-tile-speaking { - box-shadow: 0 0 0 2px rgb(52 211 153 / 0.9); +.meet-room-page [data-participant-tile].meet-tile-speaking::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 2px rgb(52 211 153 / 0.9); + pointer-events: none; + z-index: 4; } -.meet-room-page .meet-stage-spotlight:has(.meet-tile-speaking) { - box-shadow: 0 0 0 2px rgb(52 211 153 / 0.9); +.meet-room-page .meet-stage-spotlight:has(.meet-tile-speaking)::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 2px rgb(52 211 153 / 0.9); + pointer-events: none; + z-index: 4; } -.meet-room-page .meet-stage-spotlight .meet-tile-speaking { - box-shadow: none; +.meet-room-page .meet-stage-spotlight .meet-tile-speaking::after { + display: none; } .meet-room-page #meet-stage:not(.meet-stage) #meet-stage-spotlight, @@ -123,6 +135,7 @@ html.meet-room-page { } .meet-room-page [data-participant-tile] { + position: relative; display: flex; flex-direction: column; min-height: 0; @@ -237,6 +250,22 @@ html.meet-room-page { position: relative; min-height: 0; flex: 1 1 0; + overflow: hidden; +} + +.meet-room-page [data-tile-stage] { + overflow: hidden; +} + +.meet-room-page [data-tile-stage] video, +.meet-room-page [data-tile-stage] .meet-tile-video { + display: block; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + object-fit: cover; + object-position: center; } .meet-room-page [data-tile-footer] { @@ -396,12 +425,22 @@ html.meet-room-page { box-shadow: none; } -.meet-room-page .meet-space-tile.meet-tile-speaking { - box-shadow: none; +.meet-room-page .meet-space-tile.meet-tile-speaking::after { + display: none; } .meet-room-page .meet-space-tile.meet-tile-speaking [data-tile-avatar-circle] { - box-shadow: 0 0 0 2px rgb(56 189 248 / 0.85); + position: relative; +} + +.meet-room-page .meet-space-tile.meet-tile-speaking [data-tile-avatar-circle]::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 2px rgb(56 189 248 / 0.85); + pointer-events: none; + z-index: 1; } .meet-room-page .meet-space-tile--listener [data-tile-avatar-circle] { diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index 413b069..3ac4c34 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -177,6 +177,8 @@ function meetRoom() { activeSpeakerIds: [], floatingReactions: [], reactionsInitialized: false, + pictureInPictureActive: false, + pictureInPictureSupported: false, pollTimer: null, config: {}, @@ -313,6 +315,8 @@ function meetRoom() { this.syncToolbarConditionalVisibility(); document.querySelector('.meet-toolbar__track')?.classList.add('is-ready'); }); + + this.setupPictureInPicture(); }, connectionErrorMessage(error) { @@ -1054,8 +1058,8 @@ function meetRoom() { participantTileShellHtml() { return ` -
-
+
+
@@ -1682,7 +1686,7 @@ function meetRoom() { stage.innerHTML = ''; const el = track.attach(); - el.className = 'h-full w-full object-cover'; + el.className = 'meet-tile-video'; el.dataset.participant = participant.identity; stage.appendChild(el); }, @@ -2488,6 +2492,107 @@ function meetRoom() { } }, + setupPictureInPicture() { + this.pictureInPictureSupported = typeof document !== 'undefined' + && 'pictureInPictureEnabled' in document + && document.pictureInPictureEnabled !== false; + + if (!this.pictureInPictureSupported) { + return; + } + + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + this.maybeAutoEnterPictureInPicture(); + } + }); + + document.addEventListener('leavepictureinpicture', () => { + this.pictureInPictureActive = false; + }); + }, + + showPictureInPictureControl() { + return this.pictureInPictureSupported && !this.audioOnly; + }, + + findPictureInPictureVideo() { + if (this.isScreenSharing) { + const screenVideo = document.querySelector('#screen-share video'); + if (screenVideo) { + return screenVideo; + } + } + + const localId = this.config.participantUuid; + const priorities = []; + + if (room?.localParticipant?.isCameraEnabled) { + priorities.push(localId); + } + + this.activeSpeakerIds.forEach((id) => { + if (!priorities.includes(id)) { + priorities.push(id); + } + }); + + for (const id of priorities) { + const video = this.findParticipantTile(id)?.querySelector('[data-tile-stage] video'); + if (video) { + return video; + } + } + + return document.querySelector('[data-participant-tile] [data-tile-stage] video'); + }, + + async maybeAutoEnterPictureInPicture() { + if (!this.pictureInPictureSupported || this.pictureInPictureActive || this.audioOnly) { + return; + } + + if (document.pictureInPictureElement) { + return; + } + + await this.enterPictureInPicture({ silent: true }); + }, + + async togglePictureInPicture() { + if (!this.pictureInPictureSupported) { + return; + } + + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + this.pictureInPictureActive = false; + return; + } + + await this.enterPictureInPicture(); + }, + + async enterPictureInPicture({ silent = false } = {}) { + const video = this.findPictureInPictureVideo(); + if (!video) { + if (!silent) { + this.connectionStatus = 'No video available to pop out'; + } + + return; + } + + try { + await video.requestPictureInPicture(); + this.pictureInPictureActive = true; + } catch { + if (!silent) { + this.connectionStatus = 'Pop-out blocked — try again from the toolbar'; + } + } + }, + async raiseHand() { await fetch(this.config.raiseHandUrl, { method: 'POST', diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index 1d5e153..4a0cb61 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -427,6 +427,16 @@ title="Share screen"> @include('meet.room.partials.meet-icon', ['icon' => 'screen-share']) +