-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Picture-in-picture #171
Picture-in-picture #171
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()} | ||
</video> | ||
<div class="floating-buttons"></div> | ||
<div class="pip-overlay"> | ||
<svg viewBox="0 0 129 128" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M108.5 48V16a8.001 8.001 0 0 0-8-8h-84a8 8 0 0 0-8 8v68a8 8 0 0 0 8 8h20" stroke="var(--icon)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M52.5 112V72a8 8 0 0 1 8-8h52a8 8 0 0 1 8 8v40a8 8 0 0 1-8 8h-52a8 8 0 0 1-8-8Z" stroke="var(--icon)" stroke-width="3" stroke-miterlimit="10" stroke-linecap="square"/></svg> | ||
This video is playing in picture in picture | ||
</div> | ||
`; | ||
|
||
while (this.internal.root.firstChild) { | ||
|
@@ -96,6 +104,13 @@ export default class extends HTMLElement { | |
this.videoElement = this.internal.root.querySelector('video'); | ||
this.videoElement.addEventListener('error', this.handleVideoError.bind(this), true); | ||
|
||
const pipButton = this.createPiPButton(); | ||
const floatingButtonsBar = this.internal.root.querySelector('.floating-buttons'); | ||
|
||
if (pipButton) { | ||
floatingButtonsBar.appendChild(pipButton); | ||
} | ||
|
||
/** | ||
* Set up Media Session API integration. | ||
*/ | ||
|
@@ -299,4 +314,50 @@ 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.setAttribute('aria-label', 'Toggle picture in picture'); | ||
pipButton.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20.25 15L20.25 21C20.25 21.3978 20.092 21.7794 19.8107 22.0607C19.5294 22.342 19.1478 22.5 18.75 22.5L3 22.5C2.60218 22.5 2.22064 22.342 1.93934 22.0607C1.65804 21.7794 1.5 21.3978 1.5 21L1.5 8.25C1.5 7.85217 1.65804 7.47064 1.93934 7.18934C2.22064 6.90803 2.60218 6.75 3 6.75L6.75 6.75" stroke="var(--accent)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.75 3L9.75 10.5C9.75 11.3284 10.4216 12 11.25 12L21 12C21.8284 12 22.5 11.3284 22.5 10.5L22.5 3C22.5 2.17157 21.8284 1.5 21 1.5L11.25 1.5C10.4216 1.5 9.75 2.17157 9.75 3Z" stroke="var(--accent)" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="square"/><path d="M9 18.75V15H5.25" stroke="var(--accent)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.25 18.75L9 15" stroke="var(--accent)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'; | ||
|
||
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); | ||
this.videoElement.addEventListener('enterpictureinpicture', () => this.classList.add(PIP_CLASSNAME)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: It would be great if the PiP floating button would also change shape when video is playing in picture in picture. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @beaufortfrancois What do you mean by "change shape"? Like change the icon from the current "Enter PiP" icon to some kind of "Exit PiP" icon? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @beaufortfrancois Gotcha! Added in e484469. Here's a short screencast: kino-exit-pip-icon.mp4(This one's on me, our designer has originally designed both icons, but I missed it during implementation. 😐) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM. Thank you @dero! See #171 (comment) as well |
||
this.videoElement.addEventListener('leavepictureinpicture', () => this.classList.remove(PIP_CLASSNAME)); | ||
|
||
setPipButton(); | ||
|
||
return pipButton; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'd use
this.videoElement
to be explicit as it's the<video>
sub element that is in Picture-in-Picture. +1 for consistency withthis.videoElement.requestPictureInPicture()
;)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@beaufortfrancois That is not going to work, actually, as
pictureInPictureElement
can't refer to an element within ashadowRoot
tree.https://w3c.github.io/picture-in-picture/#documentorshadowroot-extension
The browser is supposed to run a retargeting algorithm to define a node that will be used instead. In this case it's the custom element itself.
It's an interesting caveat, I think. And it caught me by surprise, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, you're absolutely right. I forgot about that.