/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.polyfill.PiPWebkit');
goog.require('shaka.log');
goog.require('shaka.polyfill');
/**
* @summary A polyfill to provide PiP support in Safari.
* Note that Safari only supports PiP on video elements, not audio.
*/
shaka.polyfill.PiPWebkit = class {
/**
* Install the polyfill if needed.
*/
static install() {
if (!window.HTMLVideoElement) {
// Avoid errors on very old browsers.
return;
}
// eslint-disable-next-line no-restricted-syntax
const proto = HTMLVideoElement.prototype;
if (proto.requestPictureInPicture &&
document.exitPictureInPicture) {
// No polyfill needed.
return;
}
if (!proto.webkitSupportsPresentationMode) {
// No Webkit PiP API available.
return;
}
const PiPWebkit = shaka.polyfill.PiPWebkit;
shaka.log.debug('PiPWebkit.install');
// Polyfill document.pictureInPictureEnabled.
// It's definitely enabled now. :-)
document.pictureInPictureEnabled = true;
// Polyfill document.pictureInPictureElement.
// This is initially empty. We don't need getter or setter because we don't
// need any special handling when this is set. We assume in good faith that
// applications won't try to set this directly.
document.pictureInPictureElement = null;
// Polyfill HTMLVideoElement.requestPictureInPicture.
proto.requestPictureInPicture = PiPWebkit.requestPictureInPicture_;
// Polyfill HTMLVideoElement.disablePictureInPicture.
Object.defineProperty(proto, 'disablePictureInPicture', {
get: PiPWebkit.getDisablePictureInPicture_,
set: PiPWebkit.setDisablePictureInPicture_,
// You should be able to discover this property.
enumerable: true,
// And maybe we're not so smart. Let someone else change it if they want.
configurable: true,
});
// Polyfill document.exitPictureInPicture.
document.exitPictureInPicture = PiPWebkit.exitPictureInPicture_;
// Use the "capturing" event phase to get the webkit presentation mode event
// from the document. This way, we get the event on its way from document
// to the target element without having to intercept events in every
// possible video element.
document.addEventListener(
'webkitpresentationmodechanged', PiPWebkit.proxyEvent_,
/* useCapture= */ true);
}
/**
* @param {!Event} event
* @private
*/
static proxyEvent_(event) {
const PiPWebkit = shaka.polyfill.PiPWebkit;
const element = /** @type {!HTMLVideoElement} */(event.target);
if (element.webkitPresentationMode == PiPWebkit.PIP_MODE_) {
// Keep track of the PiP element. This element just entered PiP mode.
document.pictureInPictureElement = element;
// Dispatch a standard event to match.
const event2 = new Event('enterpictureinpicture');
element.dispatchEvent(event2);
} else {
// Keep track of the PiP element. This element just left PiP mode.
// If something else hasn't already take its place, clear it.
if (document.pictureInPictureElement == element) {
document.pictureInPictureElement = null;
}
// Dispatch a standard event to match.
const event2 = new Event('leavepictureinpicture');
element.dispatchEvent(event2);
}
}
/**
* @this {HTMLVideoElement}
* @return {!Promise}
* @private
*/
static requestPictureInPicture_() {
const PiPWebkit = shaka.polyfill.PiPWebkit;
// NOTE: "this" here is the video element.
// Check if PiP is enabled for this element.
if (!this.webkitSupportsPresentationMode(PiPWebkit.PIP_MODE_)) {
const error = new Error('PiP not allowed by video element');
return Promise.reject(error);
} else {
// Enter PiP mode.
this.webkitSetPresentationMode(PiPWebkit.PIP_MODE_);
document.pictureInPictureElement = this;
return Promise.resolve();
}
}
/**
* @this {Document}
* @return {!Promise}
* @private
*/
static exitPictureInPicture_() {
const PiPWebkit = shaka.polyfill.PiPWebkit;
const pipElement =
/** @type {HTMLVideoElement} */(document.pictureInPictureElement);
if (pipElement) {
// Exit PiP mode.
pipElement.webkitSetPresentationMode(PiPWebkit.INLINE_MODE_);
document.pictureInPictureElement = null;
return Promise.resolve();
} else {
const error = new Error('No picture in picture element found');
return Promise.reject(error);
}
}
/**
* @this {HTMLVideoElement}
* @return {boolean}
* @private
*/
static getDisablePictureInPicture_() {
// This respects the HTML attribute, which may have been set in HTML or
// through the JS setter.
if (this.hasAttribute('disablePictureInPicture')) {
return true;
}
// Use Apple's non-standard API to know if PiP is allowed on this
// device for this content. If not, say that PiP is disabled, even
// if not specified by the user through the setter or HTML attribute.
const PiPWebkit = shaka.polyfill.PiPWebkit;
return !this.webkitSupportsPresentationMode(PiPWebkit.PIP_MODE_);
}
/**
* @this {HTMLVideoElement}
* @param {boolean} value
* @private
*/
static setDisablePictureInPicture_(value) {
// This mimics how the JS setter works in browsers that implement the spec.
if (value) {
this.setAttribute('disablePictureInPicture', '');
} else {
this.removeAttribute('disablePictureInPicture');
}
}
};
/**
* The presentation mode string used to indicate PiP mode in Safari.
*
* @const {string}
* @private
*/
shaka.polyfill.PiPWebkit.PIP_MODE_ = 'picture-in-picture';
/**
* The presentation mode string used to indicate inline mode in Safari.
*
* @const {string}
* @private
*/
shaka.polyfill.PiPWebkit.INLINE_MODE_ = 'inline';
shaka.polyfill.register(shaka.polyfill.PiPWebkit.install);