aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/config/defaults.js6
-rw-r--r--src/js/controls.js23
-rw-r--r--src/js/fullscreen.js173
-rw-r--r--src/js/html5.js10
-rw-r--r--src/js/listeners.js19
-rw-r--r--src/js/plugins/preview-thumbnails.js4
-rw-r--r--src/js/plugins/vimeo.js6
-rw-r--r--src/js/plyr.d.ts63
-rw-r--r--src/js/plyr.js2
-rw-r--r--src/js/plyr.polyfilled.js2
-rw-r--r--src/js/utils/arrays.js8
-rw-r--r--src/js/utils/elements.js34
12 files changed, 196 insertions, 154 deletions
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js
index 90a74a23..bf0f8c42 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -61,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
- iconUrl: 'https://cdn.plyr.io/3.5.7-beta.0/plyr.svg',
+ iconUrl: 'https://cdn.plyr.io/3.5.7/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -69,6 +69,7 @@ const defaults = {
// Quality default
quality: {
default: 576,
+ // The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
@@ -84,7 +85,8 @@ const defaults = {
// Speed default and options to display
speed: {
selected: 1,
- options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
+ // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
+ options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
diff --git a/src/js/controls.js b/src/js/controls.js
index 15c82716..1cce51f6 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -9,7 +9,7 @@ import captions from './captions';
import html5 from './html5';
import support from './support';
import { repaint, transitionEndEvent } from './utils/animation';
-import { dedupe } from './utils/arrays';
+import { dedupe, fillRange } from './utils/arrays';
import browser from './utils/browser';
import {
createElement,
@@ -1044,7 +1044,7 @@ const controls = {
},
// Set a list of available captions languages
- setSpeedMenu(options) {
+ setSpeedMenu() {
// Menu required
if (!is.element(this.elements.settings.panels.speed)) {
return;
@@ -1053,16 +1053,14 @@ const controls = {
const type = 'speed';
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
- // Set the speed options
- if (is.array(options)) {
- this.options.speed = options;
- } else if (this.isHTML5 || this.isVimeo) {
- this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
+ // Determine options to display
+ // Vimeo and YouTube limit to 0.5x-2x
+ if (this.isVimeo || this.isYouTube) {
+ this.options.speed = fillRange(0.5, 2, 0.25).filter(s => this.config.speed.options.includes(s));
+ } else {
+ this.options.speed = this.config.speed.options;
}
- // Set options if passed and filter based on config
- this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
-
// Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleMenuButton.call(this, type, toggle);
@@ -1582,6 +1580,11 @@ const controls = {
target: '_blank',
});
+ // Set download attribute for HTML5 only
+ if (this.isHTML5) {
+ attributes.download = '';
+ }
+
const { download } = this.config.urls;
if (!is.url(download) && this.isEmbed) {
diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js
index 7ae3ff17..c74b3406 100644
--- a/src/js/fullscreen.js
+++ b/src/js/fullscreen.js
@@ -5,79 +5,10 @@
// ==========================================================================
import browser from './utils/browser';
-import { hasClass, toggleClass, trapFocus } from './utils/elements';
+import { getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
-function onChange() {
- if (!this.enabled) {
- return;
- }
-
- // Update toggle button
- const button = this.player.elements.buttons.fullscreen;
- if (is.element(button)) {
- button.pressed = this.active;
- }
-
- // Trigger an event
- triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
-
- // Trap focus in container
- if (!browser.isIos) {
- trapFocus.call(this.player, this.target, this.active);
- }
-}
-
-function toggleFallback(toggle = false) {
- // Store or restore scroll position
- if (toggle) {
- this.scrollPosition = {
- x: window.scrollX || 0,
- y: window.scrollY || 0,
- };
- } else {
- window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
- }
-
- // Toggle scroll
- document.body.style.overflow = toggle ? 'hidden' : '';
-
- // Toggle class hook
- toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
-
- // Force full viewport on iPhone X+
- if (browser.isIos) {
- let viewport = document.head.querySelector('meta[name="viewport"]');
- const property = 'viewport-fit=cover';
-
- // Inject the viewport meta if required
- if (!viewport) {
- viewport = document.createElement('meta');
- viewport.setAttribute('name', 'viewport');
- }
-
- // Check if the property already exists
- const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
-
- if (toggle) {
- this.cleanupViewport = !hasProperty;
-
- if (!hasProperty) {
- viewport.content += `,${property}`;
- }
- } else if (this.cleanupViewport) {
- viewport.content = viewport.content
- .split(',')
- .filter(part => part.trim() !== property)
- .join(',');
- }
- }
-
- // Toggle button and fire events
- onChange.call(this);
-}
-
class Fullscreen {
constructor(player) {
// Keep reference to parent
@@ -101,7 +32,7 @@ class Fullscreen {
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => {
// TODO: Filter for target??
- onChange.call(this);
+ this.onChange();
},
);
@@ -115,6 +46,9 @@ class Fullscreen {
this.toggle();
});
+ // Tap focus when in fullscreen
+ on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
+
// Update the UI
this.update();
}
@@ -194,6 +128,97 @@ class Fullscreen {
: this.player.elements.container;
}
+ onChange() {
+ if (!this.enabled) {
+ return;
+ }
+
+ // Update toggle button
+ const button = this.player.elements.buttons.fullscreen;
+ if (is.element(button)) {
+ button.pressed = this.active;
+ }
+
+ // Trigger an event
+ triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
+ }
+
+ toggleFallback(toggle = false) {
+ // Store or restore scroll position
+ if (toggle) {
+ this.scrollPosition = {
+ x: window.scrollX || 0,
+ y: window.scrollY || 0,
+ };
+ } else {
+ window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
+ }
+
+ // Toggle scroll
+ document.body.style.overflow = toggle ? 'hidden' : '';
+
+ // Toggle class hook
+ toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
+
+ // Force full viewport on iPhone X+
+ if (browser.isIos) {
+ let viewport = document.head.querySelector('meta[name="viewport"]');
+ const property = 'viewport-fit=cover';
+
+ // Inject the viewport meta if required
+ if (!viewport) {
+ viewport = document.createElement('meta');
+ viewport.setAttribute('name', 'viewport');
+ }
+
+ // Check if the property already exists
+ const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
+
+ if (toggle) {
+ this.cleanupViewport = !hasProperty;
+
+ if (!hasProperty) {
+ viewport.content += `,${property}`;
+ }
+ } else if (this.cleanupViewport) {
+ viewport.content = viewport.content
+ .split(',')
+ .filter(part => part.trim() !== property)
+ .join(',');
+ }
+ }
+
+ // Toggle button and fire events
+ this.onChange();
+ }
+
+ // Trap focus inside container
+ trapFocus(event) {
+ // Bail if iOS, not active, not the tab key
+ if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
+ return;
+ }
+
+ // Get the current focused element
+ const focused = document.activeElement;
+ const focusable = getElements.call(
+ this.player,
+ 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]',
+ );
+ const [first] = focusable;
+ const last = focusable[focusable.length - 1];
+
+ if (focused === last && !event.shiftKey) {
+ // Move focus to first element that can be tabbed if Shift isn't used
+ first.focus();
+ event.preventDefault();
+ } else if (focused === first && event.shiftKey) {
+ // Move focus to last element that can be tabbed if Shift is used
+ last.focus();
+ event.preventDefault();
+ }
+ }
+
// Update UI
update() {
if (this.enabled) {
@@ -226,9 +251,9 @@ class Fullscreen {
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
- toggleFallback.call(this, true);
+ this.toggleFallback(true);
} else if (!this.prefix) {
- this.target.requestFullscreen({ navigationUI: "hide" });
+ this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
@@ -245,7 +270,7 @@ class Fullscreen {
this.target.webkitExitFullscreen();
this.player.play();
} else if (!Fullscreen.native || this.forceFallback) {
- toggleFallback.call(this, false);
+ this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
diff --git a/src/js/html5.js b/src/js/html5.js
index 1173bcbe..d1e82489 100644
--- a/src/js/html5.js
+++ b/src/js/html5.js
@@ -65,6 +65,10 @@ const html5 = {
return source && Number(source.getAttribute('size'));
},
set(input) {
+ if (player.quality === input) {
+ return;
+ }
+
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
@@ -80,7 +84,7 @@ const html5 = {
}
// Get current state
- const { currentTime, paused, preload, readyState } = player.media;
+ const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
@@ -89,10 +93,8 @@ const html5 = {
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
- if (player.currentTime === 0) {
- return;
- }
+ player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
diff --git a/src/js/listeners.js b/src/js/listeners.js
index f68245e4..6a0046ee 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -599,12 +599,19 @@ class Listeners {
this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
// Settings menu - click toggle
- this.bind(elements.buttons.settings, 'click', event => {
- // Prevent the document click listener closing the menu
- event.stopPropagation();
+ this.bind(
+ elements.buttons.settings,
+ 'click',
+ event => {
+ // Prevent the document click listener closing the menu
+ event.stopPropagation();
+ event.preventDefault();
- controls.toggleMenu.call(player, event);
- });
+ controls.toggleMenu.call(player, event);
+ },
+ null,
+ false
+ ); // Can't be passive as we're preventing default
// Settings menu - keyboard toggle
// We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
@@ -725,7 +732,7 @@ class Listeners {
});
// Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
- this.bind(elements.progress, 'mouseleave click', () => {
+ this.bind(elements.progress, 'mouseleave touchend click', () => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {
diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js
index 8256c811..e5378bd3 100644
--- a/src/js/plugins/preview-thumbnails.js
+++ b/src/js/plugins/preview-thumbnails.js
@@ -239,8 +239,8 @@ class PreviewThumbnails {
}
startScrubbing(event) {
- // Only act on left mouse button (0), or touch device (event.button is false)
- if (event.button === false || event.button === 0) {
+ // Only act on left mouse button (0), or touch device (event.button does not exist or is false)
+ if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
this.mouseDown = true;
// Wait until media has a duration
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index 8df5ad15..9529f2cd 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -196,12 +196,6 @@ const vimeo = {
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
- })
- .catch(error => {
- // Hide menu item (and menu if empty)
- if (error.name === 'Error') {
- controls.setSpeedMenu.call(player, []);
- }
});
},
});
diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts
index 4f64898f..cd204a6f 100644
--- a/src/js/plyr.d.ts
+++ b/src/js/plyr.d.ts
@@ -6,7 +6,6 @@
export = Plyr;
export as namespace Plyr;
-
declare class Plyr {
/**
* Setup a new instance
@@ -201,17 +200,26 @@ declare class Plyr {
/**
* Add an event listener for the specified event.
*/
- on(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void;
+ on(
+ event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
+ callback: (this: this, event: Plyr.PlyrEvent) => void,
+ ): void;
/**
* Add an event listener for the specified event once.
*/
- once(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void;
+ once(
+ event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
+ callback: (this: this, event: Plyr.PlyrEvent) => void,
+ ): void;
/**
* Remove an event listener for the specified event.
*/
- off(event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent, callback: (this: this, event: Plyr.PlyrEvent) => void): void;
+ off(
+ event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
+ callback: (this: this, event: Plyr.PlyrEvent) => void,
+ ): void;
/**
* Check support for a mime type.
@@ -225,12 +233,39 @@ declare class Plyr {
}
declare namespace Plyr {
- type MediaType = "audio" | "video";
- type Provider = "html5" | "youtube" | "vimeo";
- type StandardEvent = "progress" | "playing" | "play" | "pause" | "timeupdate" | "volumechange" | "seeking" | "seeked" | "ratechange" | "ended" | "enterfullscreen" | "exitfullscreen"
- | "captionsenabled" | "captionsdisabled" | "languagechange" | "controlshidden" | "controlsshown" | "ready";
- type Html5Event = "loadstart" | "loadeddata" | "loadedmetadata" | "canplay" | "canplaythrough" | "stalled" | "waiting" | "emptied" | "cuechange" | "error";
- type YoutubeEvent = "statechange" | "qualitychange" | "qualityrequested";
+ type MediaType = 'audio' | 'video';
+ type Provider = 'html5' | 'youtube' | 'vimeo';
+ type StandardEvent =
+ | 'progress'
+ | 'playing'
+ | 'play'
+ | 'pause'
+ | 'timeupdate'
+ | 'volumechange'
+ | 'seeking'
+ | 'seeked'
+ | 'ratechange'
+ | 'ended'
+ | 'enterfullscreen'
+ | 'exitfullscreen'
+ | 'captionsenabled'
+ | 'captionsdisabled'
+ | 'languagechange'
+ | 'controlshidden'
+ | 'controlsshown'
+ | 'ready';
+ type Html5Event =
+ | 'loadstart'
+ | 'loadeddata'
+ | 'loadedmetadata'
+ | 'canplay'
+ | 'canplaythrough'
+ | 'stalled'
+ | 'waiting'
+ | 'emptied'
+ | 'cuechange'
+ | 'error';
+ type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
interface FullscreenControl {
/**
@@ -393,7 +428,7 @@ declare namespace Plyr {
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
*/
- listeners?: {[key: string]: (error: PlyrEvent) => void};
+ listeners?: { [key: string]: (error: PlyrEvent) => void };
/**
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
@@ -418,7 +453,7 @@ declare namespace Plyr {
storage?: StorageOptions;
/**
- * selected: The default speed for playback. options: Options to display in the menu. Most browsers will refuse to play slower than 0.5.
+ * selected: The default speed for playback. options: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically.
*/
speed?: SpeedOptions;
@@ -527,7 +562,7 @@ declare namespace Plyr {
size?: number;
}
- type TrackKind = "subtitles" | "captions" | "descriptions" | "chapters" | "metadata";
+ type TrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
interface Track {
/**
* Indicates how the text track is meant to be used
@@ -550,7 +585,7 @@ declare namespace Plyr {
}
interface PlyrEvent extends CustomEvent {
- readonly detail: { readonly plyr: Plyr; };
+ readonly detail: { readonly plyr: Plyr };
}
interface Support {
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 8dc2b388..b5c4612c 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
-// plyr.js v3.5.7-beta.0
+// plyr.js v3.5.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js
index 03e9a0f4..8b86a644 100644
--- a/src/js/plyr.polyfilled.js
+++ b/src/js/plyr.polyfilled.js
@@ -1,6 +1,6 @@
// ==========================================================================
// Plyr Polyfilled Build
-// plyr.js v3.5.7-beta.0
+// plyr.js v3.5.7
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
diff --git a/src/js/utils/arrays.js b/src/js/utils/arrays.js
index 69ef242c..c0d69626 100644
--- a/src/js/utils/arrays.js
+++ b/src/js/utils/arrays.js
@@ -21,3 +21,11 @@ export function closest(array, value) {
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
+
+export function fillRange(start, end, step = 1) {
+ const len = Math.floor((end - start) / step) + 1;
+
+ return Array(len)
+ .fill()
+ .map((_, idx) => start + idx * step);
+}
diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js
index 4f10938e..b88aad0c 100644
--- a/src/js/utils/elements.js
+++ b/src/js/utils/elements.js
@@ -2,7 +2,6 @@
// Element utils
// ==========================================================================
-import { toggleListener } from './events';
import is from './is';
import { extend } from './objects';
@@ -248,39 +247,6 @@ export function getElement(selector) {
return this.elements.container.querySelector(selector);
}
-// Trap focus inside container
-export function trapFocus(element = null, toggle = false) {
- if (!is.element(element)) {
- return;
- }
-
- const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
- const first = focusable[0];
- const last = focusable[focusable.length - 1];
-
- const trap = event => {
- // Bail if not tab key or not fullscreen
- if (event.key !== 'Tab' || event.keyCode !== 9) {
- return;
- }
-
- // Get the current focused element
- const focused = document.activeElement;
-
- if (focused === last && !event.shiftKey) {
- // Move focus to first element that can be tabbed if Shift isn't used
- first.focus();
- event.preventDefault();
- } else if (focused === first && event.shiftKey) {
- // Move focus to last element that can be tabbed if Shift is used
- last.focus();
- event.preventDefault();
- }
- };
-
- toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
-}
-
// Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {