fix: fullscreen improvements for iOS & iPadOS
This commit is contained in:
@ -18,8 +18,7 @@ const defaults = {
|
||||
// Only allow one media playing at once (vimeo only)
|
||||
autopause: true,
|
||||
|
||||
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
|
||||
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
|
||||
// Allow inline playback on iOS
|
||||
playsinline: true,
|
||||
|
||||
// Default time to skip when rewind/fast forward
|
||||
@ -353,7 +352,6 @@ const defaults = {
|
||||
marker: 'plyr__progress__marker',
|
||||
hidden: 'plyr__sr-only',
|
||||
hideControls: 'plyr--hide-controls',
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
|
4
src/js/controls.js
vendored
4
src/js/controls.js
vendored
@ -676,7 +676,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
if (!browser.isWebKit && !browser.isIPadOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1385,7 +1385,7 @@ const controls = {
|
||||
// Volume range control
|
||||
// Ignored on iOS as it's handled globally
|
||||
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
|
||||
if (control === 'volume' && !browser.isIos) {
|
||||
if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
|
@ -57,12 +57,10 @@ class Fullscreen {
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
|
||||
// this.toggle = this.toggle.bind(this);
|
||||
}
|
||||
|
||||
// Determine if native supported
|
||||
static get native() {
|
||||
static get nativeSupported() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
@ -72,16 +70,14 @@ class Fullscreen {
|
||||
}
|
||||
|
||||
// If we're actually using native
|
||||
get usingNative() {
|
||||
return Fullscreen.native && !this.forceFallback;
|
||||
get useNative() {
|
||||
return Fullscreen.nativeSupported && !this.forceFallback;
|
||||
}
|
||||
|
||||
// Get the prefix for handlers
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (is.function(document.exitFullscreen)) {
|
||||
return '';
|
||||
}
|
||||
if (is.function(document.exitFullscreen)) return '';
|
||||
|
||||
// Check for fullscreen support by vendor prefix
|
||||
let value = '';
|
||||
@ -103,24 +99,30 @@ class Fullscreen {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
// Determine if fullscreen is enabled
|
||||
get enabled() {
|
||||
return (
|
||||
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||
this.player.config.fullscreen.enabled &&
|
||||
this.player.supported.ui &&
|
||||
this.player.isVideo
|
||||
);
|
||||
// Determine if fullscreen is supported
|
||||
get supported() {
|
||||
return [
|
||||
// Fullscreen is enabled in config
|
||||
this.player.config.fullscreen.enabled,
|
||||
// Must be a video
|
||||
this.player.isVideo,
|
||||
// Either native is supported or fallback enabled
|
||||
Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,
|
||||
// YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline
|
||||
// must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback
|
||||
!this.player.isYouTube ||
|
||||
Fullscreen.nativeSupported ||
|
||||
!browser.isIos ||
|
||||
(this.player.config.playsinline && !this.player.config.fullscreen.iosNative),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
// Get active state
|
||||
get active() {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (!this.supported) return false;
|
||||
|
||||
// Fallback using classname
|
||||
if (!Fullscreen.native || this.forceFallback) {
|
||||
if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
@ -135,13 +137,11 @@ class Fullscreen {
|
||||
get target() {
|
||||
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||
? this.player.media
|
||||
: this.player.elements.fullscreen || this.player.elements.container;
|
||||
: this.player.elements.fullscreen ?? this.player.elements.container;
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// Update toggle button
|
||||
const button = this.player.elements.buttons.fullscreen;
|
||||
@ -159,8 +159,8 @@ class Fullscreen {
|
||||
// Store or restore scroll position
|
||||
if (toggle) {
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
x: window.scrollX ?? 0,
|
||||
y: window.scrollY ?? 0,
|
||||
};
|
||||
} else {
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
@ -188,10 +188,7 @@ class Fullscreen {
|
||||
|
||||
if (toggle) {
|
||||
this.cleanupViewport = !hasProperty;
|
||||
|
||||
if (!hasProperty) {
|
||||
viewport.content += `,${property}`;
|
||||
}
|
||||
if (!hasProperty) viewport.content += `,${property}`;
|
||||
} else if (this.cleanupViewport) {
|
||||
viewport.content = viewport.content
|
||||
.split(',')
|
||||
@ -206,10 +203,8 @@ class Fullscreen {
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus = (event) => {
|
||||
// Bail if iOS, not active, not the tab key
|
||||
if (browser.isIos || !this.active || event.key !== 'Tab') {
|
||||
return;
|
||||
}
|
||||
// Bail if iOS/iPadOS, not active, not the tab key
|
||||
if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
|
||||
|
||||
// Get the current focused element
|
||||
const focused = document.activeElement;
|
||||
@ -230,16 +225,12 @@ class Fullscreen {
|
||||
|
||||
// Update UI
|
||||
update = () => {
|
||||
if (this.enabled) {
|
||||
if (this.supported) {
|
||||
let mode;
|
||||
|
||||
if (this.forceFallback) {
|
||||
mode = 'Fallback (forced)';
|
||||
} else if (Fullscreen.native) {
|
||||
mode = 'Native';
|
||||
} else {
|
||||
mode = 'Fallback';
|
||||
}
|
||||
if (this.forceFallback) mode = 'Fallback (forced)';
|
||||
else if (Fullscreen.nativeSupported) mode = 'Native';
|
||||
else mode = 'Fallback';
|
||||
|
||||
this.player.debug.log(`${mode} fullscreen enabled`);
|
||||
} else {
|
||||
@ -247,14 +238,12 @@ class Fullscreen {
|
||||
}
|
||||
|
||||
// Add styling hook to show button
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
|
||||
};
|
||||
|
||||
// Make an element fullscreen
|
||||
enter = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// iOS native fullscreen doesn't need the request step
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
@ -263,7 +252,7 @@ class Fullscreen {
|
||||
} else {
|
||||
this.target.webkitEnterFullscreen();
|
||||
}
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
this.toggleFallback(true);
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||
@ -274,15 +263,17 @@ class Fullscreen {
|
||||
|
||||
// Bail from fullscreen
|
||||
exit = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// iOS native fullscreen
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
this.target.webkitExitFullscreen();
|
||||
if (this.player.isVimeo) {
|
||||
this.player.embed.exitFullscreen();
|
||||
} else {
|
||||
this.target.webkitEnterFullscreen();
|
||||
}
|
||||
silencePromise(this.player.play());
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
this.toggleFallback(false);
|
||||
} else if (!this.prefix) {
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
@ -294,11 +285,8 @@ class Fullscreen {
|
||||
|
||||
// Toggle state
|
||||
toggle = () => {
|
||||
if (!this.active) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
if (!this.active) this.enter();
|
||||
else this.exit();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -797,7 +797,7 @@ class Listeners {
|
||||
});
|
||||
|
||||
// Polyfill for lower fill in <input type="range"> for webkit
|
||||
if (browser.isWebkit) {
|
||||
if (browser.isWebKit) {
|
||||
Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => {
|
||||
this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
|
||||
});
|
||||
|
@ -113,7 +113,7 @@ const vimeo = {
|
||||
autoplay: player.autoplay,
|
||||
muted: player.muted,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
playsinline: player.config.playsinline,
|
||||
// hash has to be added to iframe-URL
|
||||
...hashParam,
|
||||
...frameParams,
|
||||
|
@ -131,7 +131,7 @@ const youtube = {
|
||||
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
|
||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then((image) => ui.setPoster.call(player, image.src))
|
||||
@ -161,7 +161,7 @@ const youtube = {
|
||||
// Disable keyboard as we handle it
|
||||
disablekb: 1,
|
||||
// Allow iOS inline playback
|
||||
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
|
||||
playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
|
||||
// Captions are flaky on YouTube
|
||||
cc_load_policy: player.captions.active ? 1 : 0,
|
||||
cc_lang_pref: player.config.captions.language,
|
||||
@ -183,7 +183,7 @@ const youtube = {
|
||||
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
|
||||
101: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||
150: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||
}[code] || 'An unknown error occured';
|
||||
}[code] || 'An unknown error occurred';
|
||||
|
||||
player.media.error = { code, message };
|
||||
|
||||
|
@ -246,7 +246,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Check for support again but with type
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
this.supported = support.check(this.type, this.provider);
|
||||
|
||||
// If no support for even API, bail
|
||||
if (!this.supported.api) {
|
||||
@ -1032,7 +1032,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preview thubmnails for the current source
|
||||
* Sets the preview thumbnails for the current source
|
||||
*/
|
||||
setPreviewThumbnails(thumbnailSource) {
|
||||
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
||||
@ -1239,10 +1239,9 @@ class Plyr {
|
||||
* Check for support
|
||||
* @param {String} type - Player type (audio/video)
|
||||
* @param {String} provider - Provider (html5/youtube/vimeo)
|
||||
* @param {Boolean} inline - Where player has `playsinline` sttribute
|
||||
*/
|
||||
static supported(type, provider, inline) {
|
||||
return support.check(type, provider, inline);
|
||||
static supported(type, provider) {
|
||||
return support.check(type, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,10 +24,9 @@ const support = {
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, playsinline) {
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
check(type, provider) {
|
||||
const api = support[type] || provider !== 'html5';
|
||||
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
|
||||
const ui = api && support.rangeInput;
|
||||
|
||||
return {
|
||||
api,
|
||||
|
@ -98,9 +98,6 @@ const ui = {
|
||||
// Check for airplay support
|
||||
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// Add iOS class
|
||||
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
|
||||
|
@ -3,12 +3,19 @@
|
||||
// Unfortunately, due to mixed support, UA sniffing is required
|
||||
// ==========================================================================
|
||||
|
||||
const browser = {
|
||||
isIE: Boolean(window.document.documentMode),
|
||||
isEdge: /Edge/g.test(navigator.userAgent),
|
||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent),
|
||||
isIPhone: /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1,
|
||||
isIos: /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1,
|
||||
};
|
||||
const isIE = Boolean(window.document.documentMode);
|
||||
const isEdge = /Edge/g.test(navigator.userAgent);
|
||||
const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
|
||||
const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||
// navigator.platform may be deprecated but this check is still required
|
||||
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||||
const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||
|
||||
export default browser;
|
||||
export default {
|
||||
isIE,
|
||||
isEdge,
|
||||
isWebKit,
|
||||
isIPhone,
|
||||
isIPadOS,
|
||||
isIos,
|
||||
};
|
||||
|
@ -67,7 +67,7 @@ export function createElement(type, attributes, text) {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Inaert an element after another
|
||||
// Insert an element after another
|
||||
export function insertAfter(element, target) {
|
||||
if (!is.element(element) || !is.element(target)) {
|
||||
return;
|
||||
|
@ -13,7 +13,7 @@ export function generateId(prefix) {
|
||||
export function format(input, ...args) {
|
||||
if (is.empty(input)) return input;
|
||||
|
||||
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
|
||||
return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString());
|
||||
}
|
||||
|
||||
// Get percentage
|
||||
|
@ -11,7 +11,6 @@
|
||||
@include plyr-fullscreen-active;
|
||||
|
||||
bottom: 0;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
Reference in New Issue
Block a user