From bac761b2bd8fbb72a81c2fb71b5453d6a8f9c315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Mon, 13 Dec 2021 14:58:49 +0100 Subject: [PATCH 1/4] Basic PiP button implementation. --- .../video-player/VideoPlayer.css | 11 ++++ .../video-player/VideoPlayer.js | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/js/web-components/video-player/VideoPlayer.css b/src/js/web-components/video-player/VideoPlayer.css index adf8a92..10f29ea 100644 --- a/src/js/web-components/video-player/VideoPlayer.css +++ b/src/js/web-components/video-player/VideoPlayer.css @@ -1,6 +1,7 @@ :host { display: flex; align-items: center; + position: relative; } :host *:focus { @@ -9,6 +10,16 @@ outline-color: var(--accent); } +:host button { + background: white; + color: black; + display: block; + padding: 1em; + position: absolute; + top: 1em; + right: 1em; +} + video { width: 100%; height: auto; diff --git a/src/js/web-components/video-player/VideoPlayer.js b/src/js/web-components/video-player/VideoPlayer.js index 48b12a9..efe6973 100644 --- a/src/js/web-components/video-player/VideoPlayer.js +++ b/src/js/web-components/video-player/VideoPlayer.js @@ -96,6 +96,15 @@ export default class extends HTMLElement { this.videoElement = this.internal.root.querySelector('video'); this.videoElement.addEventListener('error', this.handleVideoError.bind(this), true); + /** + * @todo Temporary. Remove when we figure out the UI. + */ + const pipButton = this.createPiPButton(); + + if (pipButton) { + this.internal.root.appendChild(pipButton); + } + /** * Set up Media Session API integration. */ @@ -299,4 +308,45 @@ export default class extends HTMLElement { assetsOnly: true, }); } + + /** + * Returns a button that controls the PiP functionality. + * + * @returns {HTMLButtonElement|null} Button element or null when PiP not supported. + */ + createPiPButton() { + if (!('pictureInPictureEnabled' in document)) { + return null; + } + + const pipButton = document.createElement('button'); + const setPipButton = () => { + pipButton.disabled = (this.videoElement.readyState === 0) + || !document.pictureInPictureEnabled + || this.videoElement.disablePictureInPicture; + }; + + pipButton.innerText = 'PiP'; + pipButton.addEventListener('click', async () => { + pipButton.disabled = true; + try { + if (this !== document.pictureInPictureElement) { + await this.videoElement.requestPictureInPicture(); + } else { + await document.exitPictureInPicture(); + } + } catch (error) { + /* eslint-disable-next-line no-console */ + console.error(error); + } finally { + pipButton.disabled = false; + } + }); + + this.videoElement.addEventListener('loadedmetadata', setPipButton); + this.videoElement.addEventListener('emptied', setPipButton); + setPipButton(); + + return pipButton; + } } From 9b2da7ba0b1ffcb9f2eab5236d6d91d352c30b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Thu, 30 Dec 2021 13:31:28 +0100 Subject: [PATCH 2/4] PiP overlay and button design. --- src/js/constants.js | 5 ++ .../video-player/VideoPlayer.css | 60 ++++++++++++++++--- .../video-player/VideoPlayer.js | 23 +++++-- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/js/constants.js b/src/js/constants.js index 90610ac..7b8b1b9 100644 --- a/src/js/constants.js +++ b/src/js/constants.js @@ -124,3 +124,8 @@ export const SETTING_KEY_DARK_MODE = 'dark-mode'; * Event name signalling that data in IDB has changes. */ export const IDB_DATA_CHANGED_EVENT = 'idb-data-changed'; + +/** + * Picture in picture. + */ +export const PIP_CLASSNAME = 'picture-in-picture'; diff --git a/src/js/web-components/video-player/VideoPlayer.css b/src/js/web-components/video-player/VideoPlayer.css index 10f29ea..9251f22 100644 --- a/src/js/web-components/video-player/VideoPlayer.css +++ b/src/js/web-components/video-player/VideoPlayer.css @@ -10,17 +10,63 @@ outline-color: var(--accent); } -:host button { - background: white; - color: black; - display: block; - padding: 1em; +:host .floating-buttons { + display: grid; + grid-auto-flow: column; + column-gap: 16px; position: absolute; - top: 1em; - right: 1em; + top: 16px; + right: 16px; + z-index: 2; +} + +@media (min-width: 720px) { + :host .floating-buttons { + top: 32px; + right: 32px; + } +} + +:host .floating-buttons button { + border-radius: 8px; + background-color: var(--accent-background); + width: 48px; + height: 48px; + display: grid; + place-items: center; + cursor: pointer; + border: none; } video { width: 100%; height: auto; } + +.pip-overlay { + display: none; + position: absolute; + inset: 0; + background-color: var(--code-background); + color: var(--icon); + z-index: 1; + font-size: clamp(12px, 4vw, 24px); +} + +.pip-overlay svg { + align-self: end; + width: clamp(40px, 20vw, 128px); + height: auto; +} + +:host(.picture-in-picture) .pip-overlay { + display: grid; + justify-items: center; + row-gap: 16px; +} + +@media (min-width: 720px) { + :host(.picture-in-picture) .pip-overlay { + row-gap: 32px; + } +} diff --git a/src/js/web-components/video-player/VideoPlayer.js b/src/js/web-components/video-player/VideoPlayer.js index efe6973..3d6571f 100644 --- a/src/js/web-components/video-player/VideoPlayer.js +++ b/src/js/web-components/video-player/VideoPlayer.js @@ -19,7 +19,10 @@ import Streamer from '../../classes/Streamer'; import ParserMPD from '../../classes/ParserMPD'; import selectSource from '../../utils/selectSource'; -import { MEDIA_SESSION_DEFAULT_ARTWORK } from '../../constants'; +import { + MEDIA_SESSION_DEFAULT_ARTWORK, + PIP_CLASSNAME, +} from '../../constants'; export default class extends HTMLElement { /** @@ -86,6 +89,11 @@ export default class extends HTMLElement { ${this.getSourceHTML()} ${this.getTracksHTML()} +
+
+ + This video is playing in picture in picture +
`; while (this.internal.root.firstChild) { @@ -96,13 +104,11 @@ export default class extends HTMLElement { this.videoElement = this.internal.root.querySelector('video'); this.videoElement.addEventListener('error', this.handleVideoError.bind(this), true); - /** - * @todo Temporary. Remove when we figure out the UI. - */ const pipButton = this.createPiPButton(); + const floatingButtonsBar = this.internal.root.querySelector('.floating-buttons'); if (pipButton) { - this.internal.root.appendChild(pipButton); + floatingButtonsBar.appendChild(pipButton); } /** @@ -326,7 +332,9 @@ export default class extends HTMLElement { || this.videoElement.disablePictureInPicture; }; - pipButton.innerText = 'PiP'; + pipButton.setAttribute('aria-label', 'Toggle picture in picture'); + pipButton.innerHTML = ''; + pipButton.addEventListener('click', async () => { pipButton.disabled = true; try { @@ -345,6 +353,9 @@ export default class extends HTMLElement { this.videoElement.addEventListener('loadedmetadata', setPipButton); this.videoElement.addEventListener('emptied', setPipButton); + this.videoElement.addEventListener('enterpictureinpicture', () => this.classList.add(PIP_CLASSNAME)); + this.videoElement.addEventListener('leavepictureinpicture', () => this.classList.remove(PIP_CLASSNAME)); + setPipButton(); return pipButton; From e4844699467affc6daee20eea7611fa4733065c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Tue, 4 Jan 2022 10:12:20 +0100 Subject: [PATCH 3/4] Add an exit PiP icon. --- src/js/web-components/video-player/VideoPlayer.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/js/web-components/video-player/VideoPlayer.js b/src/js/web-components/video-player/VideoPlayer.js index 3d6571f..4b1facf 100644 --- a/src/js/web-components/video-player/VideoPlayer.js +++ b/src/js/web-components/video-player/VideoPlayer.js @@ -324,6 +324,8 @@ export default class extends HTMLElement { if (!('pictureInPictureEnabled' in document)) { return null; } + const ENTER_PIP_SVG = ''; + const LEAVE_PIP_SVG = ''; const pipButton = document.createElement('button'); const setPipButton = () => { @@ -333,7 +335,7 @@ export default class extends HTMLElement { }; pipButton.setAttribute('aria-label', 'Toggle picture in picture'); - pipButton.innerHTML = ''; + pipButton.innerHTML = ENTER_PIP_SVG; pipButton.addEventListener('click', async () => { pipButton.disabled = true; @@ -353,8 +355,14 @@ export default class extends HTMLElement { this.videoElement.addEventListener('loadedmetadata', setPipButton); this.videoElement.addEventListener('emptied', setPipButton); - this.videoElement.addEventListener('enterpictureinpicture', () => this.classList.add(PIP_CLASSNAME)); - this.videoElement.addEventListener('leavepictureinpicture', () => this.classList.remove(PIP_CLASSNAME)); + this.videoElement.addEventListener('enterpictureinpicture', () => { + pipButton.innerHTML = LEAVE_PIP_SVG; + this.classList.add(PIP_CLASSNAME); + }); + this.videoElement.addEventListener('leavepictureinpicture', () => { + pipButton.innerHTML = ENTER_PIP_SVG; + this.classList.remove(PIP_CLASSNAME); + }); setPipButton(); From 8e34fdaf010ca75dd16815c812a12785017e1e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Polakovi=C4=8D?= Date: Tue, 4 Jan 2022 12:12:23 +0100 Subject: [PATCH 4/4] Compress the "Enter PiP" SVG icon. --- src/js/web-components/video-player/VideoPlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/web-components/video-player/VideoPlayer.js b/src/js/web-components/video-player/VideoPlayer.js index 4b1facf..ef124cb 100644 --- a/src/js/web-components/video-player/VideoPlayer.js +++ b/src/js/web-components/video-player/VideoPlayer.js @@ -324,7 +324,7 @@ export default class extends HTMLElement { if (!('pictureInPictureEnabled' in document)) { return null; } - const ENTER_PIP_SVG = ''; + const ENTER_PIP_SVG = ''; const LEAVE_PIP_SVG = ''; const pipButton = document.createElement('button');