diff --git a/demo/close_button.js b/demo/close_button.js index baae22c8d0..e89b6aedd3 100644 --- a/demo/close_button.js +++ b/demo/close_button.js @@ -31,6 +31,17 @@ shakaDemo.CloseButton = class extends shaka.ui.Element { shakaDemoMain.unload(); }); + if ('documentPictureInPicture' in window) { + this.eventManager.listen( + window.documentPictureInPicture, 'enter', () => { + this.button_.style.display = 'none'; + const pipWindow = window.documentPictureInPicture.window; + this.eventManager.listen(pipWindow, 'unload', () => { + this.button_.style.display = 'block'; + }); + }); + } + // TODO: Make sure that the screenreader description of this control is // localized! } diff --git a/demo/main.js b/demo/main.js index d35830cd1e..e8fb3b87b1 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1185,6 +1185,10 @@ shakaDemo.Main = class { if (document.pictureInPictureElement) { document.exitPictureInPicture(); } + if (window.documentPictureInPicture && + window.documentPictureInPicture.window) { + window.documentPictureInPicture.window.close(); + } this.player_.unload(); // The currently-selected asset changed, so update asset cards. diff --git a/docs/tutorials/ui-customization.md b/docs/tutorials/ui-customization.md index 8767edd6ba..c091ead1c4 100644 --- a/docs/tutorials/ui-customization.md +++ b/docs/tutorials/ui-customization.md @@ -61,7 +61,8 @@ The following elements can be added to the UI bar using this configuration value starts playing the presentation at an increased speed * spacer: adds a chunk of empty space between the adjacent elements. * picture_in_picture: adds a button that enables/disables picture-in-picture mode on browsers - that support it. Button is invisible on other browsers. + that support it. Button is invisible on other browsers. Note that it will use the + [Document Picture-in-Picture API]() if supported. * loop: adds a button that controls if the currently selected video is played in a loop. * airplay: adds a button that opens a AirPlay dialog. The button is visible only if the browser supports AirPlay. @@ -72,6 +73,7 @@ The following elements can be added to the UI bar using this configuration value * playback_rate: adds a button that controls the playback rate selection. * captions: adds a button that controls the current text track selection (including turning it off). +[Document Picture-in-Picture API]: https://developer.chrome.com/docs/web-platform/document-picture-in-picture/ Similarly, the 'overflowMenuButtons' configuration option can be used to control the contents of the overflow menu. @@ -83,7 +85,8 @@ The following buttons can be added to the overflow menu: * quality: adds a button that controls enabling/disabling of abr and video resolution selection. * language: adds a button that controls audio language selection. * picture_in_picture: adds a button that enables/disables picture-in-picture mode on browsers - that support it. Button is invisible on other browsers. + that support it. Button is invisible on other browsers. Note that it will use the + [Document Picture-in-Picture API]() if supported. * loop: adds a button that controls if the currently selected video is played in a loop. * playback_rate: adds a button that controls the playback rate selection. * airplay: adds a button that opens a AirPlay dialog. The button is visible only if the browser @@ -122,7 +125,8 @@ The following buttons can be added to the context menu: * Statistics: adds a button that displays statistics of the video. * loop: adds a button that controls if the currently selected video is played in a loop. * picture_in_picture: adds a button that enables/disables picture-in-picture mode on browsers - that support it. Button is invisible on other browsers. + that support it. Button is invisible on other browsers. Note that it will use the + [Document Picture-in-Picture API]() if supported. Example: ```js diff --git a/externs/pictureinpicture.js b/externs/pictureinpicture.js index 6bd16b6b4c..12f92e193a 100644 --- a/externs/pictureinpicture.js +++ b/externs/pictureinpicture.js @@ -49,3 +49,53 @@ HTMLMediaElement.prototype.webkitSupportsPresentationMode = function(mode) {}; /** @type {string} */ HTMLMediaElement.prototype.webkitPresentationMode; + + +/** + * @typedef {{ + * initialAspectRatio: (number|undefined), + * width: (number|undefined), + * height: (number|undefined), + * copyStyleSheets: (boolean|undefined), + * }} + */ +var DocumentPictureInPictureOptions; + + +/** + * @constructor + * @implements {EventTarget} + */ +function DocumentPictureInPicture() {} + + +/** + * @param {DocumentPictureInPictureOptions} options + * @return {!Promise.} + */ +DocumentPictureInPicture.prototype.requestWindow = function(options) {}; + + +/** @type {Window} */ +DocumentPictureInPicture.prototype.window; + + +/** @override */ +DocumentPictureInPicture.prototype.addEventListener = + function(type, listener, options) {}; + + +/** @override */ +DocumentPictureInPicture.prototype.removeEventListener = + function(type, listener, options) {}; + + +/** @override */ +DocumentPictureInPicture.prototype.dispatchEvent = function(event) {}; + + +/** + * @see https://wicg.github.io/document-picture-in-picture/#api + * @type {!DocumentPictureInPicture} + */ +Window.prototype.documentPictureInPicture; diff --git a/ui/pip_button.js b/ui/pip_button.js index c688d2e2cd..f33aa216ce 100644 --- a/ui/pip_button.js +++ b/ui/pip_button.js @@ -36,6 +36,9 @@ shaka.ui.PipButton = class extends shaka.ui.Element { /** @private {HTMLMediaElement} */ this.localVideo_ = this.controls.getLocalVideo(); + /** @private {HTMLElement } */ + this.videoContainer_ = this.controls.getVideoContainer(); + const LocIds = shaka.ui.Locales.Ids; /** @private {!HTMLButtonElement} */ this.pipButton_ = shaka.util.Dom.createButton(); @@ -111,8 +114,8 @@ shaka.ui.PipButton = class extends shaka.ui.Element { * @private */ isPipAllowed_() { - return document.pictureInPictureEnabled && - !this.video.disablePictureInPicture; + return ('documentPictureInPicture' in window) || + (document.pictureInPictureEnabled && !this.video.disablePictureInPicture); } @@ -122,6 +125,10 @@ shaka.ui.PipButton = class extends shaka.ui.Element { */ async onPipClick_() { try { + if ('documentPictureInPicture' in window) { + await this.toggleDocumentPictureInPicture_(); + return; + } if (!document.pictureInPictureElement) { // If you were fullscreen, leave fullscreen first. if (document.fullscreenElement) { @@ -137,6 +144,50 @@ shaka.ui.PipButton = class extends shaka.ui.Element { } } + /** + * The Document Picture-in-Picture API makes it possible to open an + * always-on-top window that can be populated with arbitrary HTML content. + * https://developer.chrome.com/docs/web-platform/document-picture-in-picture + * @private + */ + async toggleDocumentPictureInPicture_() { + // Close Picture-in-Picture window if any. + if (window.documentPictureInPicture.window) { + window.documentPictureInPicture.window.close(); + this.onLeavePictureInPicture_(); + return; + } + + // Open a Picture-in-Picture window. + const pipPlayer = this.videoContainer_; + const rectPipPlayer = pipPlayer.getBoundingClientRect(); + const pipWindow = await window.documentPictureInPicture.requestWindow({ + initialAspectRatio: rectPipPlayer.width / rectPipPlayer.height, + copyStyleSheets: true, + }); + + // Add placeholder for the player. + const parentPlayer = pipPlayer.parentNode || document.body; + const placeholder = this.videoContainer_.cloneNode(true); + placeholder.style.visibility = 'hidden'; + placeholder.style.height = getComputedStyle(pipPlayer).height; + parentPlayer.appendChild(placeholder); + + // Make sure player fits in the Picture-in-Picture window. + const styles = document.createElement('style'); + styles.append(`[data-shaka-player-container] { + width: 100% !important; max-height: 100%}`); + pipWindow.document.head.append(styles); + + // Move player to the Picture-in-Picture window. + pipWindow.document.body.append(pipPlayer); + this.onEnterPictureInPicture_(); + + // Listen for the PiP closing event to move the player back. + this.eventManager.listenOnce(pipWindow, 'unload', () => { + placeholder.replaceWith(/** @type {!Node} */(pipPlayer)); + }); + } /** @private */ onEnterPictureInPicture_() {