From da0cc530f66913cd7317af352241692d477208c5 Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Sat, 11 Jan 2025 18:53:02 +0100 Subject: [PATCH] [mirotalksfu] - add Document PIP --- README.md | 2 +- app/src/config.template.js | 1 + public/css/DocumentPiP.css | 27 ++++++++ public/js/Room.js | 19 ++++++ public/js/RoomClient.js | 128 +++++++++++++++++++++++++++++++++++++ public/js/Rules.js | 5 +- public/views/Room.html | 1 + 7 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 public/css/DocumentPiP.css diff --git a/README.md b/README.md index 5d53a670..2ef06406 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ - File sharing with drag-and-drop support. - Choose your audio input, output, and video source. - Supports video quality up to 4K. -- Supports advance Picture-in-Picture (PiP) offering a more streamlined and flexible viewing experience. +- Supports advance Video/Document Picture-in-Picture (PiP) offering a more streamlined and flexible viewing experience. - Record your screen, audio, and video locally or on your Server. - Snapshot video frames and save them as PNG images. - Chat with an Emoji Picker for expressing feelings, private messages, Markdown support, and conversation saving. diff --git a/app/src/config.template.js b/app/src/config.template.js index 052852cb..b7ac5492 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -457,6 +457,7 @@ module.exports = { raiseHandButton: true, transcriptionButton: true, whiteboardButton: true, + documentPiPButton: true, snapshotRoomButton: true, emojiRoomButton: true, settingsButton: true, diff --git a/public/css/DocumentPiP.css b/public/css/DocumentPiP.css new file mode 100644 index 00000000..2cce250c --- /dev/null +++ b/public/css/DocumentPiP.css @@ -0,0 +1,27 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +body { + background: var(--body-bg); +} + +.pipVideoContainer { + display: grid; + gap: 10px; +} + +.pipVideo { + width: 100%; + object-fit: cover; + border-radius: 5px; + aspect-ratio: 16 / 9; +} + +.mirror { + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + transform: rotateY(180deg); +} diff --git a/public/js/Room.js b/public/js/Room.js index b1a5e94f..a0f56a03 100644 --- a/public/js/Room.js +++ b/public/js/Room.js @@ -31,6 +31,9 @@ const isIPadDevice = parserResult.device.model?.toLowerCase() === 'ipad'; const isDesktopDevice = deviceType === 'desktop'; const thisInfo = getInfo(); +const isEmbedded = window.self !== window.top; +const showDocumentPipBtn = !isEmbedded && 'documentPictureInPicture' in window; + const socket = io({ transports: ['websocket'], reconnection: isDesktopDevice, @@ -412,6 +415,7 @@ function refreshMainButtonsToolTipPlacement() { setTippy('editorButton', 'Toggle the editor', placement); setTippy('transcriptionButton', 'Toggle transcription', placement); setTippy('whiteboardButton', 'Toggle the whiteboard', placement); + setTippy('documentPiPButton', 'Toggle Document picture in picture', placement); setTippy('snapshotRoomButton', 'Snapshot screen, window, or tab', placement); setTippy('settingsButton', 'Toggle the settings', placement); setTippy('restartICEButton', 'Restart ICE', placement); @@ -1506,6 +1510,7 @@ function roomIsReady() { show(fullScreenButton); } BUTTONS.main.whiteboardButton && show(whiteboardButton); + if (BUTTONS.main.documentPiPButton && showDocumentPipBtn) show(documentPiPButton); BUTTONS.main.settingsButton && show(settingsButton); isAudioAllowed ? show(stopAudioButton) : BUTTONS.main.startAudioButton && show(startAudioButton); isVideoAllowed ? show(stopVideoButton) : BUTTONS.main.startVideoButton && show(startVideoButton); @@ -2044,6 +2049,9 @@ function handleButtons() { whiteboardButton.onclick = () => { toggleWhiteboard(); }; + documentPiPButton.onclick = () => { + rc.toggleDocumentPIP(); + }; snapshotRoomButton.onclick = () => { rc.snapshotRoom(); }; @@ -2872,6 +2880,17 @@ function handleSelects() { } whiteboardButton.click(); break; + case 'd': + if (notPresenter && !BUTTONS.main.documentPiPButton) { + userLog( + 'warning', + 'The presenter has disabled your ability to open the document PIP', + 'top-end', + ); + break; + } + documentPiPButton.click(); + break; case 'j': if (notPresenter && !BUTTONS.main.emojiRoomButton) { userLog('warning', 'The presenter has disabled your ability to open the room emoji', 'top-end'); diff --git a/public/js/RoomClient.js b/public/js/RoomClient.js index 4e21c40c..e9309933 100644 --- a/public/js/RoomClient.js +++ b/public/js/RoomClient.js @@ -3512,6 +3512,134 @@ class RoomClient { } } + // #################################################### + // HANDLE DOCUMENT PIP + // #################################################### + + async toggleDocumentPIP() { + if (documentPictureInPicture.window) { + documentPictureInPicture.window.close(); + console.log('DOCUMENT PIP close'); + return; + } + await this.documentPictureInPictureOpen(); + } + + documentPictureInPictureClose() { + if (!showDocumentPipBtn) return; + if (documentPictureInPicture.window) { + documentPictureInPicture.window.close(); + console.log('DOCUMENT PIP close'); + } + } + + async documentPictureInPictureOpen() { + if (!showDocumentPipBtn) return; + try { + const pipWindow = await documentPictureInPicture.requestWindow({ + width: 300, + height: 720, + }); + + function updateCustomProperties() { + const documentStyle = getComputedStyle(document.documentElement); + + pipWindow.document.documentElement.style = ` + --body-bg: ${documentStyle.getPropertyValue('--body-bg')}; + `; + } + + updateCustomProperties(); + + const pipStylesheet = document.createElement('link'); + const pipVideoContainer = document.createElement('div'); + + pipStylesheet.type = 'text/css'; + pipStylesheet.rel = 'stylesheet'; + pipStylesheet.href = '../css/DocumentPiP.css'; + + pipVideoContainer.className = 'pipVideoContainer'; + + pipWindow.document.head.append(pipStylesheet); + pipWindow.document.body.append(pipVideoContainer); + + function cloneVideoElements() { + let foundVideo = false; + + pipVideoContainer.innerHTML = ''; + + [...document.querySelectorAll('video')].forEach((video) => { + console.log('DOCUMENT PIP found video id -----> ' + video.id); + + // No video stream detected or is video share from URL... + if (!video.srcObject || video.id === '__videoShare') return; + + let videoPIPAllowed = false; + + // get video element + const videoPlayer = rc.getId(video.id); + + // Check if video can be add on pipVideo + if ([rc.videoProducerId, rc.screenProducerId].includes(video.id)) { + // PRODUCER + videoPIPAllowed = !videoPlayer.classList.contains('videoCircle'); // not in privacy mode + console.log('DOCUMENT PIP PRODUCER videoPIPAllowed -----> ' + videoPIPAllowed); + } else { + // CONSUMER + videoPIPAllowed = !videoPlayer.classList.contains('videoCircle'); // not in privacy mode + console.log('DOCUMENT PIP CONAUMER videoPIPAllowed -----> ' + videoPIPAllowed); + } + + if (!videoPIPAllowed) return; + + // Video is ON not in privacy mode continue.... + + foundVideo = true; + + const pipVideo = document.createElement('video'); + + pipVideo.classList.add('pipVideo'); + pipVideo.classList.toggle('mirror', video.classList.contains('mirror')); + pipVideo.srcObject = video.srcObject; + pipVideo.autoplay = true; + pipVideo.muted = true; + + pipVideoContainer.append(pipVideo); + }); + + return foundVideo; + } + + if (!cloneVideoElements()) { + rc.documentPictureInPictureClose(); + return userLog('warning', 'No video allowed for Document PIP', 'top-end', 6000); + } + + const videoObserver = new MutationObserver(() => { + cloneVideoElements(); + }); + + videoObserver.observe(rc.videoMediaContainer, { + childList: true, + }); + + const documentObserver = new MutationObserver(() => { + updateCustomProperties(); + }); + + documentObserver.observe(document.documentElement, { + attributeFilter: ['style'], + }); + + pipWindow.addEventListener('unload', () => { + videoObserver.disconnect(); + documentObserver.disconnect(); + }); + } catch (err) { + userLog('warning', err.message, 'top-end', 6000); + } + } + // #################################################### // FULL SCREEN // #################################################### diff --git a/public/js/Rules.js b/public/js/Rules.js index 0df87f65..4023d7ae 100644 --- a/public/js/Rules.js +++ b/public/js/Rules.js @@ -23,10 +23,11 @@ let BUTTONS = { swapCameraButton: true, chatButton: true, pollButton: true, + editorButton: true, raiseHandButton: true, transcriptionButton: true, whiteboardButton: true, - editorButton: true, + documentPiPButton: true, snapshotRoomButton: true, emojiRoomButton: true, settingsButton: true, @@ -240,6 +241,7 @@ function handleRulesBroadcasting() { BUTTONS.main.swapCameraButton = false; //BUTTONS.main.raiseHandButton = false; BUTTONS.main.whiteboardButton = false; + BUTTONS.main.documentPiPButton = false; //BUTTONS.main.snapshotRoomButton = false; //BUTTONS.main.emojiRoomButton = false, //BUTTONS.main.pollButton = false; @@ -275,6 +277,7 @@ function handleRulesBroadcasting() { elemDisplay('swapCameraButton', false); //elemDisplay('raiseHandButton', false); elemDisplay('whiteboardButton', false); + elemDisplay('documentPiPButton', false); //elemDisplay('snapshotRoomButton', false); //elemDisplay('emojiRoomButton', false); //elemDisplay('pollButton', false); diff --git a/public/views/Room.html b/public/views/Room.html index e90016fe..9dc96a0a 100644 --- a/public/views/Room.html +++ b/public/views/Room.html @@ -192,6 +192,7 @@ access to use this app. +