Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PiP pollyfill #1902

Merged
merged 11 commits into from May 3, 2019
Merged
1 change: 1 addition & 0 deletions build/types/polyfill
Expand Up @@ -8,6 +8,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
28 changes: 28 additions & 0 deletions externs/pictureinpicture.js
Expand Up @@ -34,6 +34,10 @@ HTMLDocument.prototype.pictureInPictureElement;
HTMLDocument.prototype.pictureInPictureEnabled;


/** @type {Element} */
HTMLDocument.prototype.polyfillPictureInPictureElement;
avelad marked this conversation as resolved.
Show resolved Hide resolved


/**
* @return {!Promise}
*/
Expand All @@ -42,3 +46,27 @@ HTMLMediaElement.prototype.requestPictureInPicture = function() {};

/** @type {boolean} */
HTMLMediaElement.prototype.disablePictureInPicture;


/** @type {Function} */
HTMLMediaElement.prototype.webkitSetPresentationMode;


/** @type {Function} */
avelad marked this conversation as resolved.
Show resolved Hide resolved
HTMLMediaElement.prototype.webkitSupportsPresentationMode;


/** @type {string} */
HTMLMediaElement.prototype.webkitPresentationMode;


/** @type {Object} */
HTMLMediaElement.prototype.polyfillEnterpictureinpicture;
avelad marked this conversation as resolved.
Show resolved Hide resolved


/** @type {Object} */
HTMLMediaElement.prototype.polyfillLeavepictureinpicture;


/** @type {string} */
HTMLMediaElement.prototype.polyfillPreviousPresentationMode;
245 changes: 245 additions & 0 deletions lib/polyfill/pip.js
@@ -0,0 +1,245 @@
/**
* @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.
*
* @this {HTMLMediaElement}
avelad marked this conversation as resolved.
Show resolved Hide resolved
*/
shaka.polyfill.PiP.install = function() {
if (!window.HTMLVideoElement) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
// Avoid errors on very old browsers.
return;
}

let proto = HTMLVideoElement.prototype;
avelad marked this conversation as resolved.
Show resolved Hide resolved
if (proto.requestPictureInPicture &&
document.exitPictureInPicture) {
// No polyfill needed.
return;
}

if (proto.webkitSupportsPresentationMode &&
avelad marked this conversation as resolved.
Show resolved Hide resolved
typeof proto.webkitSetPresentationMode === 'function') {
shaka.log.debug('PiP.install');

/**
* polyfill document.pictureInPictureElement
*/
Object.defineProperty(document, 'pictureInPictureElement', {
avelad marked this conversation as resolved.
Show resolved Hide resolved
get() {
if (!document.polyfillPictureInPictureElement) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
const videoElementList = document.querySelectorAll('video');
avelad marked this conversation as resolved.
Show resolved Hide resolved

for (let i = 0; i < videoElementList.length; i += 1) {
const video =
/** @type {!HTMLMediaElement} */ (videoElementList[i]);

if (video.webkitPresentationMode &&
video.webkitPresentationMode === 'picture-in-picture') {
avelad marked this conversation as resolved.
Show resolved Hide resolved
document.pictureInPictureElement = video;
avelad marked this conversation as resolved.
Show resolved Hide resolved
break;
}
}
}

return document.polyfillPictureInPictureElement || null;
},

set(value) {
if (value === document.polyfillPictureInPictureElement) return;

if (document.polyfillPictureInPictureElement) {
document.polyfillPictureInPictureElement.removeEventListener(
'webkitpresentationmodechanged',
shaka.polyfill.PiP.updatePictureInPictureElementInDocument,
);
}

document.polyfillPictureInPictureElement =
/** @type {!HTMLMediaElement} */ (value);
if (document.polyfillPictureInPictureElement) {
document.polyfillPictureInPictureElement.addEventListener(
'webkitpresentationmodechanged',
shaka.polyfill.PiP.updatePictureInPictureElementInDocument,
);
}
},
});

/**
* polyfill document.pictureInPictureEnabled
*/
document.pictureInPictureEnabled = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always the case? Are there instances where the API is present, but Safari might not support PiP at runtime for some reason?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried and the API it in both MSE and src = so I have not seen any reason for this to not be true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sounds good.


/**
* 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;
proto.addEventListener = function(...args) {
avelad marked this conversation as resolved.
Show resolved Hide resolved
const [name, callback] = args;

if (name === 'enterpictureinpicture') {
avelad marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-inner-declarations
function enterpictureinpictureListener() {
if (this.webkitPresentationMode === 'picture-in-picture') {
callback();
}
}
this.addEventListener('webkitpresentationmodechanged',
enterpictureinpictureListener);

// keep track of the listener to be able to remove them later
if (this.polyfillEnterpictureinpicture) {
this.polyfillEnterpictureinpicture[callback] =
enterpictureinpictureListener;
} else {
this.polyfillEnterpictureinpicture = {
[callback]: enterpictureinpictureListener,
};
}
} else if (name === 'leavepictureinpicture') {
// eslint-disable-next-line no-inner-declarations
function leavepictureinpictureListener() {
if (this.webkitPresentationMode === 'inline') {
if (this.polyfillPreviousPresentationMode ===
'picture-in-picture') {
callback();
}
} else {
// keep track of the pipElement
document.pictureInPictureElement = this;
}
this.polyfillPreviousPresentationMode = this.webkitPresentationMode;
}
this.addEventListener('webkitpresentationmodechanged',
leavepictureinpictureListener);

// keep track of the listener to be able to remove them later
if (this.polyfillLeavepictureinpicture) {
this.polyfillLeavepictureinpicture[callback] =
leavepictureinpictureListener;
} else {
this.polyfillLeavepictureinpicture = {
[callback]: leavepictureinpictureListener,
};
}
} else {
// fallback for all the other events
oldAddEventListener.apply(this, args);
}
};

const oldRemoveEventListener = proto.removeEventListener;
proto.removeEventListener = function(...args) {
const [name, callback] = args;

if (name === 'enterpictureinpicture' &&
this.polyfillEnterpictureinpicture) {
this.removeEventListener(
'webkitpresentationmodechanged',
this.polyfillEnterpictureinpicture[callback],
);
delete this.polyfillEnterpictureinpicture[callback];
} else if (name === 'leavepictureinpicture' &&
this.polyfillLeavepictureinpicture) {
this.removeEventListener(
'webkitpresentationmodechanged',
this.polyfillLeavepictureinpicture[callback],
);
delete this.polyfillLeavepictureinpicture[callback];
} else {
oldRemoveEventListener.apply(this, args);
}
};
}
};

/**
* @this {HTMLMediaElement}
avelad marked this conversation as resolved.
Show resolved Hide resolved
* @private
*/
shaka.polyfill.PiP.updatePictureInPictureElementInDocument = function() {
avelad marked this conversation as resolved.
Show resolved Hide resolved
if (this.webkitPresentationMode &&
this.webkitPresentationMode !== 'picture-in-picture') {
document.pictureInPictureElement = null;
}
};

/**
* @this {HTMLMediaElement}
* @return {!Promise}
* @private
*/
shaka.polyfill.PiP.requestPictureInPicture = function() {
avelad marked this conversation as resolved.
Show resolved Hide resolved
// 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();
}
};

/**
* @return {!Promise}
* @private
avelad marked this conversation as resolved.
Show resolved Hide resolved
*/
shaka.polyfill.PiP.exitPictureInPicture = function() {
avelad marked this conversation as resolved.
Show resolved Hide resolved
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);
1 change: 1 addition & 0 deletions shaka-player.uncompiled.js
Expand Up @@ -50,6 +50,7 @@ goog.require('shaka.polyfill.MediaSource');
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');
Expand Down