fix: fullscreen improvements for iOS & iPadOS

This commit is contained in:
Sam Potts
2023-03-09 22:31:27 +11:00
parent 5731245f4f
commit 62436d8e8e
14 changed files with 159 additions and 167 deletions

View File

@ -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
View File

@ -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,

View File

@ -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();
};
}

View File

@ -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));
});

View File

@ -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,

View File

@ -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 };

View File

@ -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);
}
/**

View File

@ -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,

View File

@ -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);

View File

@ -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,
};

View File

@ -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;

View File

@ -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

View File

@ -11,7 +11,6 @@
@include plyr-fullscreen-active;
bottom: 0;
display: block;
left: 0;
position: fixed;
right: 0;