Source: lib/polyfill/pip_webkit.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.polyfill.PiPWebkit');
  7. goog.require('shaka.log');
  8. goog.require('shaka.polyfill');
  9. /**
  10. * @summary A polyfill to provide PiP support in Safari.
  11. * Note that Safari only supports PiP on video elements, not audio.
  12. * @export
  13. */
  14. shaka.polyfill.PiPWebkit = class {
  15. /**
  16. * Install the polyfill if needed.
  17. * @export
  18. */
  19. static install() {
  20. if (!window.HTMLVideoElement) {
  21. // Avoid errors on very old browsers.
  22. return;
  23. }
  24. // eslint-disable-next-line no-restricted-syntax
  25. const proto = HTMLVideoElement.prototype;
  26. if (proto.requestPictureInPicture &&
  27. document.exitPictureInPicture) {
  28. // No polyfill needed.
  29. return;
  30. }
  31. if (!proto.webkitSupportsPresentationMode) {
  32. // No Webkit PiP API available.
  33. return;
  34. }
  35. const PiPWebkit = shaka.polyfill.PiPWebkit;
  36. shaka.log.debug('PiPWebkit.install');
  37. // Polyfill document.pictureInPictureEnabled.
  38. // It's definitely enabled now. :-)
  39. document.pictureInPictureEnabled = true;
  40. // Polyfill document.pictureInPictureElement.
  41. // This is initially empty. We don't need getter or setter because we don't
  42. // need any special handling when this is set. We assume in good faith that
  43. // applications won't try to set this directly.
  44. document.pictureInPictureElement = null;
  45. // Polyfill HTMLVideoElement.requestPictureInPicture.
  46. proto.requestPictureInPicture = PiPWebkit.requestPictureInPicture_;
  47. // Polyfill HTMLVideoElement.disablePictureInPicture.
  48. Object.defineProperty(proto, 'disablePictureInPicture', {
  49. get: PiPWebkit.getDisablePictureInPicture_,
  50. set: PiPWebkit.setDisablePictureInPicture_,
  51. // You should be able to discover this property.
  52. enumerable: true,
  53. // And maybe we're not so smart. Let someone else change it if they want.
  54. configurable: true,
  55. });
  56. // Polyfill document.exitPictureInPicture.
  57. document.exitPictureInPicture = PiPWebkit.exitPictureInPicture_;
  58. // Use the "capturing" event phase to get the webkit presentation mode event
  59. // from the document. This way, we get the event on its way from document
  60. // to the target element without having to intercept events in every
  61. // possible video element.
  62. document.addEventListener(
  63. 'webkitpresentationmodechanged', PiPWebkit.proxyEvent_,
  64. /* useCapture= */ true);
  65. }
  66. /**
  67. * @param {!Event} event
  68. * @private
  69. */
  70. static proxyEvent_(event) {
  71. const PiPWebkit = shaka.polyfill.PiPWebkit;
  72. const element = /** @type {!HTMLVideoElement} */(event.target);
  73. if (element.webkitPresentationMode == PiPWebkit.PIP_MODE_) {
  74. // Keep track of the PiP element. This element just entered PiP mode.
  75. document.pictureInPictureElement = element;
  76. // Dispatch a standard event to match.
  77. const event2 = new Event('enterpictureinpicture');
  78. element.dispatchEvent(event2);
  79. } else {
  80. // Keep track of the PiP element. This element just left PiP mode.
  81. // If something else hasn't already take its place, clear it.
  82. if (document.pictureInPictureElement == element) {
  83. document.pictureInPictureElement = null;
  84. }
  85. // Dispatch a standard event to match.
  86. const event2 = new Event('leavepictureinpicture');
  87. element.dispatchEvent(event2);
  88. }
  89. }
  90. /**
  91. * @this {HTMLVideoElement}
  92. * @return {!Promise}
  93. * @private
  94. */
  95. static requestPictureInPicture_() {
  96. const PiPWebkit = shaka.polyfill.PiPWebkit;
  97. // NOTE: "this" here is the video element.
  98. // Check if PiP is enabled for this element.
  99. if (!this.webkitSupportsPresentationMode(PiPWebkit.PIP_MODE_)) {
  100. const error = new Error('PiP not allowed by video element');
  101. return Promise.reject(error);
  102. } else {
  103. // Enter PiP mode.
  104. this.webkitSetPresentationMode(PiPWebkit.PIP_MODE_);
  105. document.pictureInPictureElement = this;
  106. return Promise.resolve();
  107. }
  108. }
  109. /**
  110. * @this {Document}
  111. * @return {!Promise}
  112. * @private
  113. */
  114. static exitPictureInPicture_() {
  115. const PiPWebkit = shaka.polyfill.PiPWebkit;
  116. const pipElement =
  117. /** @type {HTMLVideoElement} */(document.pictureInPictureElement);
  118. if (pipElement) {
  119. // Exit PiP mode.
  120. pipElement.webkitSetPresentationMode(PiPWebkit.INLINE_MODE_);
  121. document.pictureInPictureElement = null;
  122. return Promise.resolve();
  123. } else {
  124. const error = new Error('No picture in picture element found');
  125. return Promise.reject(error);
  126. }
  127. }
  128. /**
  129. * @this {HTMLVideoElement}
  130. * @return {boolean}
  131. * @private
  132. */
  133. static getDisablePictureInPicture_() {
  134. // This respects the HTML attribute, which may have been set in HTML or
  135. // through the JS setter.
  136. if (this.hasAttribute('disablePictureInPicture')) {
  137. return true;
  138. }
  139. // Use Apple's non-standard API to know if PiP is allowed on this
  140. // device for this content. If not, say that PiP is disabled, even
  141. // if not specified by the user through the setter or HTML attribute.
  142. const PiPWebkit = shaka.polyfill.PiPWebkit;
  143. return !this.webkitSupportsPresentationMode(PiPWebkit.PIP_MODE_);
  144. }
  145. /**
  146. * @this {HTMLVideoElement}
  147. * @param {boolean} value
  148. * @private
  149. */
  150. static setDisablePictureInPicture_(value) {
  151. // This mimics how the JS setter works in browsers that implement the spec.
  152. if (value) {
  153. this.setAttribute('disablePictureInPicture', '');
  154. } else {
  155. this.removeAttribute('disablePictureInPicture');
  156. }
  157. }
  158. };
  159. /**
  160. * The presentation mode string used to indicate PiP mode in Safari.
  161. *
  162. * @const {string}
  163. * @private
  164. */
  165. shaka.polyfill.PiPWebkit.PIP_MODE_ = 'picture-in-picture';
  166. /**
  167. * The presentation mode string used to indicate inline mode in Safari.
  168. *
  169. * @const {string}
  170. * @private
  171. */
  172. shaka.polyfill.PiPWebkit.INLINE_MODE_ = 'inline';
  173. shaka.polyfill.register(shaka.polyfill.PiPWebkit.install);