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