diff --git a/build/types/polyfill b/build/types/polyfill index 8cbbc9b70..2425064d5 100644 --- a/build/types/polyfill +++ b/build/types/polyfill @@ -10,6 +10,7 @@ +../../lib/polyfill/patchedmediakeys_ms.js +../../lib/polyfill/patchedmediakeys_nop.js +../../lib/polyfill/patchedmediakeys_webkit.js ++../../lib/polyfill/pip.js +../../lib/polyfill/video_play_promise.js +../../lib/polyfill/videoplaybackquality.js +../../lib/polyfill/vttcue.js diff --git a/externs/pictureinpicture.js b/externs/pictureinpicture.js index 2d4ac213e..0c3e547b5 100644 --- a/externs/pictureinpicture.js +++ b/externs/pictureinpicture.js @@ -42,3 +42,21 @@ HTMLMediaElement.prototype.requestPictureInPicture = function() {}; /** @type {boolean} */ HTMLMediaElement.prototype.disablePictureInPicture; + + +/** + * @param {string} mode + * @return {boolean} + */ +HTMLMediaElement.prototype.webkitSetPresentationMode = function(mode) {}; + + +/** + * @param {string} mode + * @return {boolean} + */ +HTMLMediaElement.prototype.webkitSupportsPresentationMode = function(mode) {}; + + +/** @type {string} */ +HTMLMediaElement.prototype.webkitPresentationMode; diff --git a/lib/polyfill/pip.js b/lib/polyfill/pip.js new file mode 100644 index 000000000..090f925f2 --- /dev/null +++ b/lib/polyfill/pip.js @@ -0,0 +1,232 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('shaka.polyfill.PiP'); + +goog.require('shaka.log'); +goog.require('shaka.polyfill.register'); + + +/** + * @namespace shaka.polyfill.PiP + * + * @summary A polyfill to provide PiP support in Safari. + */ + + +/** + * Install the polyfill if needed. + */ +shaka.polyfill.PiP.install = function() { + if (!window.HTMLVideoElement) { + // Avoid errors on very old browsers. + return; + } + + const proto = HTMLVideoElement.prototype; + if (proto.requestPictureInPicture && + document.exitPictureInPicture) { + // No polyfill needed. + return; + } + + if (!proto.webkitSupportsPresentationMode) { + // No polyfill available. + return; + } + shaka.log.debug('PiP.install'); + + /** @type {HTMLMediaElement} */ + let polyfillPictureInPictureElement = null; + + let polyfillEnterpictureinpicture = null; + let polyfillLeavepictureinpicture = null; + + /** + * polyfill document.pictureInPictureElement + */ + Object.defineProperty(document, 'pictureInPictureElement', { + get() { + return polyfillPictureInPictureElement; + }, + + set(value) { + if (value === polyfillPictureInPictureElement) return; + + if (polyfillPictureInPictureElement) { + polyfillPictureInPictureElement.removeEventListener( + 'webkitpresentationmodechanged', + shaka.polyfill.PiP.updatePictureInPictureElementInDocument_, + ); + } + + polyfillPictureInPictureElement = + /** @type {HTMLMediaElement} */ (value); + if (polyfillPictureInPictureElement) { + polyfillPictureInPictureElement.addEventListener( + 'webkitpresentationmodechanged', + shaka.polyfill.PiP.updatePictureInPictureElementInDocument_, + ); + } + }, + }); + + /** + * polyfill document.pictureInPictureEnabled + */ + document.pictureInPictureEnabled = true; + + /** + * polyfill HTMLMediaElement.requestPictureInPicture + */ + proto.requestPictureInPicture = shaka.polyfill.PiP.requestPictureInPicture_; + + + /** + * polyfill document.exitPictureInPicture + */ + document.exitPictureInPicture = shaka.polyfill.PiP.exitPictureInPicture_; + + /** + * polyfill enterpictureinpicture and leavepictureinpicture events + */ + + const oldAddEventListener = proto.addEventListener; + /** + * @this {HTMLMediaElement} + */ + proto.addEventListener = function(type, listener, options) { + const callback = /** @type {Function} */ (listener); + if (type === 'enterpictureinpicture') { + // eslint-disable-next-line no-inner-declarations + function proxyEnterPiPEvent(event) { + const videoElement = + /** @type {HTMLVideoElement} */ (event.target); + if (videoElement.webkitPresentationMode === 'picture-in-picture') { + // keep track of the pipElement + document.pictureInPictureElement = videoElement; + callback(); + } + } + this.addEventListener('webkitpresentationmodechanged', + proxyEnterPiPEvent); + + // keep track of the listener to be able to remove them later + if (polyfillEnterpictureinpicture) { + polyfillEnterpictureinpicture[listener] = proxyEnterPiPEvent; + } else { + polyfillEnterpictureinpicture = { + [listener]: proxyEnterPiPEvent, + }; + } + } else if (type === 'leavepictureinpicture') { + // eslint-disable-next-line no-inner-declarations + function proxyLeavePiPEvent(event) { + const videoElement = + /** @type {HTMLVideoElement} */ (event.target); + if (videoElement.webkitPresentationMode === 'inline') { + callback(); + } + } + this.addEventListener('webkitpresentationmodechanged', + proxyLeavePiPEvent); + + // keep track of the listener to be able to remove them later + if (polyfillLeavepictureinpicture) { + polyfillLeavepictureinpicture[listener] = proxyLeavePiPEvent; + } else { + polyfillLeavepictureinpicture = { + [listener]: proxyLeavePiPEvent, + }; + } + } else { + // fallback for all the other events + oldAddEventListener.apply(this, [type, listener, options]); + } + }; + + const oldRemoveEventListener = proto.removeEventListener; + /** + * @this {HTMLMediaElement} + */ + proto.removeEventListener = function(type, listener, options) { + if (type === 'enterpictureinpicture') { + this.removeEventListener('webkitpresentationmodechanged', + polyfillEnterpictureinpicture[listener]); + } else if (type === 'leavepictureinpicture') { + this.removeEventListener('webkitpresentationmodechanged', + polyfillLeavepictureinpicture[listener]); + } else { + // fallback for all the other events + oldRemoveEventListener.apply(this, [type, listener, options]); + } + }; +}; + +/** + * @param {!Event} event + * @private + */ +shaka.polyfill.PiP.updatePictureInPictureElementInDocument_ = + function(event) { + const videoElement = /** @type {HTMLVideoElement} */ (event.target); + if (videoElement.webkitPresentationMode && + videoElement.webkitPresentationMode !== 'picture-in-picture') { + document.pictureInPictureElement = null; + } +}; + +/** + * @this {HTMLMediaElement} + * @return {!Promise} + * @private + */ +shaka.polyfill.PiP.requestPictureInPicture_ = function() { + // check if PIP is enabled + if (!this.webkitSupportsPresentationMode('picture-in-picture')) { + const error = new Error('PIP not allowed by videoElement', + 'InvalidStateError'); + return Promise.reject(error); + } else { + // enter PIP mode + this.webkitSetPresentationMode('picture-in-picture'); + document.pictureInPictureElement = this; + return Promise.resolve(); + } +}; + +/** + * @this {Document} + * @return {!Promise} + * @private + */ +shaka.polyfill.PiP.exitPictureInPicture_ = function() { + if (document.pictureInPictureElement) { + // exit PIP mode + const video = + /** @type {!HTMLMediaElement} */ (document.pictureInPictureElement); + video.webkitSetPresentationMode('inline'); + document.pictureInPictureElement = null; + return Promise.resolve(); + } else { + const error = new Error('No picture in picture element found', + 'InvalidStateError'); + return Promise.reject(error); + } +}; + +shaka.polyfill.register(shaka.polyfill.PiP.install); diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index 3f9fc0d42..c2c8ab80f 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -51,6 +51,7 @@ goog.require('shaka.polyfill.PatchedMediaKeysApple'); goog.require('shaka.polyfill.PatchedMediaKeysMs'); goog.require('shaka.polyfill.PatchedMediaKeysNop'); goog.require('shaka.polyfill.PatchedMediaKeysWebkit'); +goog.require('shaka.polyfill.PiP'); goog.require('shaka.polyfill.VTTCue'); goog.require('shaka.polyfill.VideoPlayPromise'); goog.require('shaka.polyfill.VideoPlaybackQuality');