aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/captions.js34
-rw-r--r--src/js/config/defaults.js20
-rw-r--r--src/js/controls.js576
-rw-r--r--src/js/fullscreen.js4
-rw-r--r--src/js/html5.js11
-rw-r--r--src/js/listeners.js52
-rw-r--r--src/js/plugins/ads.js109
-rw-r--r--src/js/plugins/preview-thumbnails.js (renamed from src/js/plugins/previewThumbnails.js)38
-rw-r--r--src/js/plugins/vimeo.js14
-rw-r--r--src/js/plugins/youtube.js106
-rw-r--r--src/js/plyr.js202
-rw-r--r--src/js/plyr.polyfilled.js3
-rw-r--r--src/js/ui.js44
-rw-r--r--src/js/utils/animation.js16
-rw-r--r--src/js/utils/elements.js29
-rw-r--r--src/js/utils/events.js1
-rw-r--r--src/js/utils/i18n.js4
-rw-r--r--src/js/utils/load-image.js (renamed from src/js/utils/loadImage.js)0
-rw-r--r--src/js/utils/load-script.js (renamed from src/js/utils/loadScript.js)0
-rw-r--r--src/js/utils/load-sprite.js (renamed from src/js/utils/loadSprite.js)3
-rw-r--r--src/js/utils/numbers.js17
-rw-r--r--src/js/utils/style.js63
-rw-r--r--src/js/utils/time.js1
-rw-r--r--src/js/utils/urls.js4
-rw-r--r--src/sass/components/control.scss5
-rw-r--r--src/sass/components/controls.scss64
-rw-r--r--src/sass/components/embed.scss36
-rw-r--r--src/sass/components/progress.scss15
-rw-r--r--src/sass/components/video.scss33
-rw-r--r--src/sass/lib/mixins.scss3
-rw-r--r--src/sass/plugins/ads.scss2
-rw-r--r--src/sass/plugins/preview-thumbnails.scss (renamed from src/sass/plugins/previewThumbnails.scss)2
-rw-r--r--src/sass/plyr.scss3
-rw-r--r--src/sass/settings/badges.scss2
-rw-r--r--src/sass/settings/colors.scss18
-rw-r--r--src/sass/settings/controls.scss2
-rw-r--r--src/sass/settings/menus.scss4
-rw-r--r--src/sass/settings/progress.scss4
-rw-r--r--src/sass/settings/sliders.scss2
-rw-r--r--src/sass/settings/tooltips.scss2
40 files changed, 868 insertions, 680 deletions
diff --git a/src/js/captions.js b/src/js/captions.js
index ae4642aa..e33fd81a 100644
--- a/src/js/captions.js
+++ b/src/js/captions.js
@@ -85,7 +85,6 @@ const captions = {
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
-
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto'
@@ -124,19 +123,22 @@ const captions = {
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
- tracks.filter(track => !meta.get(track)).forEach(track => {
- this.debug.log('Track added', track);
- // Attempt to store if the original dom element was "default"
- meta.set(track, {
- default: track.mode === 'showing',
+ tracks
+ .filter(track => !meta.get(track))
+ .forEach(track => {
+ this.debug.log('Track added', track);
+ // Attempt to store if the original dom element was "default"
+ meta.set(track, {
+ default: track.mode === 'showing',
+ });
+
+ // Turn off native caption rendering to avoid double captions
+ // eslint-disable-next-line no-param-reassign
+ track.mode = 'hidden';
+
+ // Add event listener for cue changes
+ on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
});
-
- // Turn off native caption rendering to avoid double captions
- track.mode = 'hidden';
-
- // Add event listener for cue changes
- on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
- });
}
// Update language first time it matches, or if the previous matching track was removed
@@ -164,7 +166,6 @@ const captions = {
const { toggled } = this.captions; // Current state
const activeClass = this.config.classNames.captions.active;
-
// Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
@@ -300,10 +301,12 @@ const captions = {
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
+
languages.every(language => {
- track = sorted.find(track => track.language === language);
+ track = sorted.find(t => t.language === language);
return !track; // Break iteration if there is a match
});
+
// If no match is found but is required, get first
return track || (force ? sorted[0] : undefined);
},
@@ -360,6 +363,7 @@ const captions = {
// Get cues from track
if (!cues) {
const track = captions.getCurrentTrack.call(this);
+
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js
index a3859638..c50a8900 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -42,8 +42,9 @@ const defaults = {
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
- // Aspect ratio (for embeds)
- ratio: '16:9',
+ // Force an aspect ratio
+ // The format must be `'w:h'` (e.g. `'16:9'`)
+ ratio: null,
// Click video container to play/pause
clickToPlay: true,
@@ -60,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
- iconUrl: 'https://cdn.plyr.io/3.5.0-beta.5/plyr.svg',
+ iconUrl: 'https://cdn.plyr.io/3.5.6/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -127,6 +128,7 @@ const defaults = {
// 'fast-forward',
'progress',
'current-time',
+ // 'duration',
'mute',
'volume',
'captions',
@@ -194,8 +196,7 @@ const defaults = {
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
- api:
- 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
+ api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -319,9 +320,6 @@ const defaults = {
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
- menu: {
- quality: '.js-plyr__menu__list--quality',
- },
},
// Class hooks added to the player in different states
@@ -330,6 +328,7 @@ const defaults = {
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
+ videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
@@ -394,11 +393,6 @@ const defaults = {
},
},
- // API keys
- keys: {
- google: null,
- },
-
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
diff --git a/src/js/controls.js b/src/js/controls.js
index 73903e16..7afcd2c0 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -4,6 +4,7 @@
// ==========================================================================
import RangeTouch from 'rangetouch';
+
import captions from './captions';
import html5 from './html5';
import support from './support';
@@ -27,7 +28,7 @@ import {
import { off, on } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
-import loadSprite from './utils/loadSprite';
+import loadSprite from './utils/load-sprite';
import { extend } from './utils/objects';
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
import { formatTime, getHours } from './utils/time';
@@ -105,7 +106,6 @@ const controls = {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
-
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
setAttributes(
@@ -172,7 +172,7 @@ const controls = {
// Create a <button>
createButton(buttonType, attr) {
- const attributes = Object.assign({}, attr);
+ const attributes = extend({}, attr);
let type = toCamelCase(buttonType);
const props = {
@@ -198,8 +198,10 @@ const controls = {
// Set class name
if (Object.keys(attributes).includes('class')) {
- if (!attributes.class.includes(this.config.classNames.control)) {
- attributes.class += ` ${this.config.classNames.control}`;
+ if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
+ extend(attributes, {
+ class: `${attributes.class} ${this.config.classNames.control}`,
+ });
}
} else {
attributes.class = this.config.classNames.control;
@@ -377,13 +379,13 @@ const controls = {
},
// Create time display
- createTime(type) {
- const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
+ createTime(type, attrs) {
+ const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
const container = createElement(
'div',
extend(attributes, {
- class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(),
+ class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
'aria-label': i18n.get(type, this.config),
}),
'00:00',
@@ -491,15 +493,15 @@ const controls = {
get() {
return menuItem.getAttribute('aria-checked') === 'true';
},
- set(checked) {
+ set(check) {
// Ensure exclusivity
- if (checked) {
+ if (check) {
Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false'));
}
- menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
+ menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
},
});
@@ -607,17 +609,17 @@ const controls = {
let value = 0;
const setProgress = (target, input) => {
- const value = is.number(input) ? input : 0;
+ const val = is.number(input) ? input : 0;
const progress = is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (is.element(progress)) {
- progress.value = value;
+ progress.value = val;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (is.element(label)) {
- label.childNodes[0].nodeValue = value;
+ label.childNodes[0].nodeValue = val;
}
}
};
@@ -699,14 +701,8 @@ const controls = {
return;
}
- // Calculate percentage
- let percent = 0;
- const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
-
- const toggle = toggle => {
- toggleClass(this.elements.display.seekTooltip, visible, toggle);
- };
+ const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
// Hide on touch
if (this.touch) {
@@ -715,6 +711,9 @@ const controls = {
}
// Determine percentage, if already visible
+ let percent = 0;
+ const clientRect = this.elements.progress.getBoundingClientRect();
+
if (is.event(event)) {
percent = (100 / clientRect.width) * (event.pageX - clientRect.left);
} else if (hasClass(this.elements.display.seekTooltip, visible)) {
@@ -1111,7 +1110,7 @@ const controls = {
let target = pane;
if (!is.element(target)) {
- target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
+ target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
}
const firstItem = target.querySelector('[role^="menuitem"]');
@@ -1138,7 +1137,10 @@ const controls = {
} else if (is.keyboardEvent(input) && input.which === 27) {
show = false;
} else if (is.event(input)) {
- const isMenuItem = popup.contains(input.target);
+ // If Plyr is in a shadowDOM, the event target is set to the component, instead of the
+ // Element in the shadowDOM. The path, if available, is complete.
+ const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
+ const isMenuItem = popup.contains(target);
// If the click was inside the menu or if the click
// wasn't the button or menu item and we're trying to
@@ -1191,7 +1193,7 @@ const controls = {
// Show a panel in the menu
showMenuPanel(type = '', tabFocus = false) {
- const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
+ const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
// Nothing to show, bail
if (!is.element(target)) {
@@ -1244,8 +1246,8 @@ const controls = {
controls.focusFirstMenuItem.call(this, target, tabFocus);
},
- // Set the download link
- setDownloadLink() {
+ // Set the download URL
+ setDownloadUrl() {
const button = this.elements.buttons.download;
// Bail if no button
@@ -1253,324 +1255,356 @@ const controls = {
return;
}
- // Set download link
+ // Set attribute
button.setAttribute('href', this.download);
},
// Build the default HTML
- // TODO: Set order based on order in the config.controls array?
create(data) {
- // Create the container
- const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
+ const {
+ bindMenuItemShortcuts,
+ createButton,
+ createProgress,
+ createRange,
+ createTime,
+ setQualityMenu,
+ setSpeedMenu,
+ showMenuPanel,
+ } = controls;
+ this.elements.controls = null;
- // Restart button
- if (this.config.controls.includes('restart')) {
- container.appendChild(controls.createButton.call(this, 'restart'));
+ // Larger overlaid play button
+ if (this.config.controls.includes('play-large')) {
+ this.elements.container.appendChild(createButton.call(this, 'play-large'));
}
- // Rewind button
- if (this.config.controls.includes('rewind')) {
- container.appendChild(controls.createButton.call(this, 'rewind'));
- }
+ // Create the container
+ const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
+ this.elements.controls = container;
- // Play/Pause button
- if (this.config.controls.includes('play')) {
- container.appendChild(controls.createButton.call(this, 'play'));
- }
+ // Default item attributes
+ const defaultAttributes = { class: 'plyr__controls__item' };
- // Fast forward button
- if (this.config.controls.includes('fast-forward')) {
- container.appendChild(controls.createButton.call(this, 'fast-forward'));
- }
+ // Loop through controls in order
+ dedupe(this.config.controls).forEach(control => {
+ // Restart button
+ if (control === 'restart') {
+ container.appendChild(createButton.call(this, 'restart', defaultAttributes));
+ }
- // Progress
- if (this.config.controls.includes('progress')) {
- const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
+ // Rewind button
+ if (control === 'rewind') {
+ container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
+ }
- // Seek range slider
- progress.appendChild(
- controls.createRange.call(this, 'seek', {
- id: `plyr-seek-${data.id}`,
- }),
- );
+ // Play/Pause button
+ if (control === 'play') {
+ container.appendChild(createButton.call(this, 'play', defaultAttributes));
+ }
+
+ // Fast forward button
+ if (control === 'fast-forward') {
+ container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
+ }
- // Buffer progress
- progress.appendChild(controls.createProgress.call(this, 'buffer'));
+ // Progress
+ if (control === 'progress') {
+ const progressContainer = createElement('div', {
+ class: `${defaultAttributes.class} plyr__progress__container`,
+ });
- // TODO: Add loop display indicator
+ const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
- // Seek tooltip
- if (this.config.tooltips.seek) {
- const tooltip = createElement(
- 'span',
- {
- class: this.config.classNames.tooltip,
- },
- '00:00',
+ // Seek range slider
+ progress.appendChild(
+ createRange.call(this, 'seek', {
+ id: `plyr-seek-${data.id}`,
+ }),
);
- progress.appendChild(tooltip);
- this.elements.display.seekTooltip = tooltip;
- }
+ // Buffer progress
+ progress.appendChild(createProgress.call(this, 'buffer'));
- this.elements.progress = progress;
- container.appendChild(this.elements.progress);
- }
+ // TODO: Add loop display indicator
- // Media current time display
- if (this.config.controls.includes('current-time')) {
- container.appendChild(controls.createTime.call(this, 'currentTime'));
- }
-
- // Media duration display
- if (this.config.controls.includes('duration')) {
- container.appendChild(controls.createTime.call(this, 'duration'));
- }
+ // Seek tooltip
+ if (this.config.tooltips.seek) {
+ const tooltip = createElement(
+ 'span',
+ {
+ class: this.config.classNames.tooltip,
+ },
+ '00:00',
+ );
- // Volume controls
- if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
- const volume = createElement('div', {
- class: 'plyr__volume',
- });
+ progress.appendChild(tooltip);
+ this.elements.display.seekTooltip = tooltip;
+ }
- // Toggle mute button
- if (this.config.controls.includes('mute')) {
- volume.appendChild(controls.createButton.call(this, 'mute'));
+ this.elements.progress = progress;
+ progressContainer.appendChild(this.elements.progress);
+ container.appendChild(progressContainer);
}
- // Volume range control
- if (this.config.controls.includes('volume')) {
- // Set the attributes
- const attributes = {
- max: 1,
- step: 0.05,
- value: this.config.volume,
- };
-
- // Create the volume range slider
- volume.appendChild(
- controls.createRange.call(
- this,
- 'volume',
- extend(attributes, {
- id: `plyr-volume-${data.id}`,
- }),
- ),
- );
-
- this.elements.volume = volume;
+ // Media current time display
+ if (control === 'current-time') {
+ container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
}
- container.appendChild(volume);
- }
-
- // Toggle captions button
- if (this.config.controls.includes('captions')) {
- container.appendChild(controls.createButton.call(this, 'captions'));
- }
+ // Media duration display
+ if (control === 'duration') {
+ container.appendChild(createTime.call(this, 'duration', defaultAttributes));
+ }
- // Settings button / menu
- if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
- const control = createElement('div', {
- class: 'plyr__menu',
- hidden: '',
- });
+ // Volume controls
+ if (control === 'mute' || control === 'volume') {
+ let { volume } = this.elements;
- control.appendChild(
- controls.createButton.call(this, 'settings', {
- 'aria-haspopup': true,
- 'aria-controls': `plyr-settings-${data.id}`,
- 'aria-expanded': false,
- }),
- );
+ // Create the volume container if needed
+ if (!is.element(volume) || !container.contains(volume)) {
+ volume = createElement(
+ 'div',
+ extend({}, defaultAttributes, {
+ class: `${defaultAttributes.class} plyr__volume`.trim(),
+ }),
+ );
- const popup = createElement('div', {
- class: 'plyr__menu__container',
- id: `plyr-settings-${data.id}`,
- hidden: '',
- });
+ this.elements.volume = volume;
- const inner = createElement('div');
+ container.appendChild(volume);
+ }
- const home = createElement('div', {
- id: `plyr-settings-${data.id}-home`,
- });
+ // Toggle mute button
+ if (control === 'mute') {
+ volume.appendChild(createButton.call(this, 'mute'));
+ }
- // Create the menu
- const menu = createElement('div', {
- role: 'menu',
- });
+ // Volume range control
+ if (control === 'volume') {
+ // Set the attributes
+ const attributes = {
+ max: 1,
+ step: 0.05,
+ value: this.config.volume,
+ };
+
+ // Create the volume range slider
+ volume.appendChild(
+ createRange.call(
+ this,
+ 'volume',
+ extend(attributes, {
+ id: `plyr-volume-${data.id}`,
+ }),
+ ),
+ );
+ }
+ }
- home.appendChild(menu);
- inner.appendChild(home);
- this.elements.settings.panels.home = home;
+ // Toggle captions button
+ if (control === 'captions') {
+ container.appendChild(createButton.call(this, 'captions', defaultAttributes));
+ }
- // Build the menu items
- this.config.settings.forEach(type => {
- // TODO: bundle this with the createMenuItem helper and bindings
- const menuItem = createElement(
- 'button',
- extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
- type: 'button',
- class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
- role: 'menuitem',
- 'aria-haspopup': true,
+ // Settings button / menu
+ if (control === 'settings' && !is.empty(this.config.settings)) {
+ const wrapper = createElement(
+ 'div',
+ extend({}, defaultAttributes, {
+ class: `${defaultAttributes.class} plyr__menu`.trim(),
hidden: '',
}),
);
- // Bind menu shortcuts for keyboard users
- controls.bindMenuItemShortcuts.call(this, menuItem, type);
+ wrapper.appendChild(
+ createButton.call(this, 'settings', {
+ 'aria-haspopup': true,
+ 'aria-controls': `plyr-settings-${data.id}`,
+ 'aria-expanded': false,
+ }),
+ );
- // Show menu on click
- on(menuItem, 'click', () => {
- controls.showMenuPanel.call(this, type, false);
+ const popup = createElement('div', {
+ class: 'plyr__menu__container',
+ id: `plyr-settings-${data.id}`,
+ hidden: '',
});
- const flex = createElement('span', null, i18n.get(type, this.config));
+ const inner = createElement('div');
- const value = createElement('span', {
- class: this.config.classNames.menu.value,
+ const home = createElement('div', {
+ id: `plyr-settings-${data.id}-home`,
});
- // Speed contains HTML entities
- value.innerHTML = data[type];
+ // Create the menu
+ const menu = createElement('div', {
+ role: 'menu',
+ });
- flex.appendChild(value);
- menuItem.appendChild(flex);
- menu.appendChild(menuItem);
+ home.appendChild(menu);
+ inner.appendChild(home);
+ this.elements.settings.panels.home = home;
+
+ // Build the menu items
+ this.config.settings.forEach(type => {
+ // TODO: bundle this with the createMenuItem helper and bindings
+ const menuItem = createElement(
+ 'button',
+ extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
+ type: 'button',
+ class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
+ role: 'menuitem',
+ 'aria-haspopup': true,
+ hidden: '',
+ }),
+ );
- // Build the panes
- const pane = createElement('div', {
- id: `plyr-settings-${data.id}-${type}`,
- hidden: '',
- });
+ // Bind menu shortcuts for keyboard users
+ bindMenuItemShortcuts.call(this, menuItem, type);
- // Back button
- const backButton = createElement('button', {
- type: 'button',
- class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
- });
+ // Show menu on click
+ on(menuItem, 'click', () => {
+ showMenuPanel.call(this, type, false);
+ });
- // Visible label
- backButton.appendChild(
- createElement(
- 'span',
- {
- 'aria-hidden': true,
- },
- i18n.get(type, this.config),
- ),
- );
+ const flex = createElement('span', null, i18n.get(type, this.config));
- // Screen reader label
- backButton.appendChild(
- createElement(
- 'span',
- {
- class: this.config.classNames.hidden,
- },
- i18n.get('menuBack', this.config),
- ),
- );
+ const value = createElement('span', {
+ class: this.config.classNames.menu.value,
+ });
- // Go back via keyboard
- on(
- pane,
- 'keydown',
- event => {
- // We only care about <-
- if (event.which !== 37) {
- return;
- }
+ // Speed contains HTML entities
+ value.innerHTML = data[type];
- // Prevent seek
- event.preventDefault();
- event.stopPropagation();
+ flex.appendChild(value);
+ menuItem.appendChild(flex);
+ menu.appendChild(menuItem);
- // Show the respective menu
- controls.showMenuPanel.call(this, 'home', true);
- },
- false,
- );
+ // Build the panes
+ const pane = createElement('div', {
+ id: `plyr-settings-${data.id}-${type}`,
+ hidden: '',
+ });
- // Go back via button click
- on(backButton, 'click', () => {
- controls.showMenuPanel.call(this, 'home', false);
- });
+ // Back button
+ const backButton = createElement('button', {
+ type: 'button',
+ class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
+ });
+
+ // Visible label
+ backButton.appendChild(
+ createElement(
+ 'span',
+ {
+ 'aria-hidden': true,
+ },
+ i18n.get(type, this.config),
+ ),
+ );
+
+ // Screen reader label
+ backButton.appendChild(
+ createElement(
+ 'span',
+ {
+ class: this.config.classNames.hidden,
+ },
+ i18n.get('menuBack', this.config),
+ ),
+ );
+
+ // Go back via keyboard
+ on(
+ pane,
+ 'keydown',
+ event => {
+ // We only care about <-
+ if (event.which !== 37) {
+ return;
+ }
- // Add to pane
- pane.appendChild(backButton);
+ // Prevent seek
+ event.preventDefault();
+ event.stopPropagation();
- // Menu
- pane.appendChild(
- createElement('div', {
- role: 'menu',
- }),
- );
+ // Show the respective menu
+ showMenuPanel.call(this, 'home', true);
+ },
+ false,
+ );
- inner.appendChild(pane);
+ // Go back via button click
+ on(backButton, 'click', () => {
+ showMenuPanel.call(this, 'home', false);
+ });
- this.elements.settings.buttons[type] = menuItem;
- this.elements.settings.panels[type] = pane;
- });
+ // Add to pane
+ pane.appendChild(backButton);
- popup.appendChild(inner);
- control.appendChild(popup);
- container.appendChild(control);
+ // Menu
+ pane.appendChild(
+ createElement('div', {
+ role: 'menu',
+ }),
+ );
- this.elements.settings.popup = popup;
- this.elements.settings.menu = control;
- }
+ inner.appendChild(pane);
- // Picture in picture button
- if (this.config.controls.includes('pip') && support.pip) {
- container.appendChild(controls.createButton.call(this, 'pip'));
- }
+ this.elements.settings.buttons[type] = menuItem;
+ this.elements.settings.panels[type] = pane;
+ });
- // Airplay button
- if (this.config.controls.includes('airplay') && support.airplay) {
- container.appendChild(controls.createButton.call(this, 'airplay'));
- }
+ popup.appendChild(inner);
+ wrapper.appendChild(popup);
+ container.appendChild(wrapper);
- // Download button
- if (this.config.controls.includes('download')) {
- const attributes = {
- element: 'a',
- href: this.download,
- target: '_blank',
- };
+ this.elements.settings.popup = popup;
+ this.elements.settings.menu = wrapper;
+ }
- const { download } = this.config.urls;
+ // Picture in picture button
+ if (control === 'pip' && support.pip) {
+ container.appendChild(createButton.call(this, 'pip', defaultAttributes));
+ }
- if (!is.url(download) && this.isEmbed) {
- extend(attributes, {
- icon: `logo-${this.provider}`,
- label: this.provider,
- });
+ // Airplay button
+ if (control === 'airplay' && support.airplay) {
+ container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
}
- container.appendChild(controls.createButton.call(this, 'download', attributes));
- }
+ // Download button
+ if (control === 'download') {
+ const attributes = extend({}, defaultAttributes, {
+ element: 'a',
+ href: this.download,
+ target: '_blank',
+ });
- // Toggle fullscreen button
- if (this.config.controls.includes('fullscreen')) {
- container.appendChild(controls.createButton.call(this, 'fullscreen'));
- }
+ const { download } = this.config.urls;
- // Larger overlaid play button
- if (this.config.controls.includes('play-large')) {
- this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
- }
+ if (!is.url(download) && this.isEmbed) {
+ extend(attributes, {
+ icon: `logo-${this.provider}`,
+ label: this.provider,
+ });
+ }
- this.elements.controls = container;
+ container.appendChild(createButton.call(this, 'download', attributes));
+ }
+
+ // Toggle fullscreen button
+ if (control === 'fullscreen') {
+ container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
+ }
+ });
// Set available quality levels
if (this.isHTML5) {
- controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
+ setQualityMenu.call(this, html5.getQualityOptions.call(this));
}
- controls.setSpeedMenu.call(this);
+ setSpeedMenu.call(this);
return container;
},
diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js
index c86bf877..4de8da88 100644
--- a/src/js/fullscreen.js
+++ b/src/js/fullscreen.js
@@ -4,7 +4,6 @@
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
// ==========================================================================
-import { repaint } from './utils/animation';
import browser from './utils/browser';
import { hasClass, toggleClass, trapFocus } from './utils/elements';
import { on, triggerEvent } from './utils/events';
@@ -73,9 +72,6 @@ function toggleFallback(toggle = false) {
.filter(part => part.trim() !== property)
.join(',');
}
-
- // Force a repaint as sometimes Safari doesn't want to fill the screen
- setTimeout(() => repaint(this.target), 100);
}
// Toggle button and fire events
diff --git a/src/js/html5.js b/src/js/html5.js
index 3266a58e..b03e9c26 100644
--- a/src/js/html5.js
+++ b/src/js/html5.js
@@ -6,6 +6,7 @@ import support from './support';
import { removeElement } from './utils/elements';
import { triggerEvent } from './utils/events';
import is from './utils/is';
+import { setAspectRatio } from './utils/style';
const html5 = {
getSources() {
@@ -43,12 +44,17 @@ const html5 = {
const player = this;
+ // Set aspect ratio if fixed
+ if (!is.empty(this.config.ratio)) {
+ setAspectRatio.call(player);
+ }
+
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
- const source = sources.find(source => source.getAttribute('src') === player.source);
+ const source = sources.find(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
@@ -56,9 +62,8 @@ const html5 = {
set(input) {
// Get sources
const sources = html5.getSources.call(player);
-
// Get first match for requested size
- const source = sources.find(source => Number(source.getAttribute('size')) === input);
+ const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found
if (!source) {
diff --git a/src/js/listeners.js b/src/js/listeners.js
index 3c65b824..354dc605 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -9,7 +9,7 @@ import browser from './utils/browser';
import { getElement, getElements, matches, toggleClass, toggleHidden } from './utils/elements';
import { off, on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is';
-import { setAspectRatio } from './utils/style';
+import { getAspectRatio, setAspectRatio } from './utils/style';
class Listeners {
constructor(player) {
@@ -147,7 +147,7 @@ class Listeners {
player.loop = !player.loop;
break;
- /* case 73:
+ /* case 73:
this.setLoop('start');
break;
@@ -275,17 +275,16 @@ class Listeners {
elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => {
- const { controls } = elements;
+ const { controls: controlsElement } = elements;
// Remove button states for fullscreen
- if (controls && event.type === 'enterfullscreen') {
- controls.pressed = false;
- controls.hover = false;
+ if (controlsElement && event.type === 'enterfullscreen') {
+ controlsElement.pressed = false;
+ controlsElement.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
-
let delay = 0;
if (show) {
@@ -302,14 +301,6 @@ class Listeners {
},
);
- // Force edge to repaint on exit fullscreen
- // TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
- /* if (browser.isEdge) {
- on.call(player, elements.container, 'exitfullscreen', () => {
- setTimeout(() => repaint(elements.container), 100);
- });
- } */
-
// Set a gutter for Vimeo
const setGutter = (ratio, padding, toggle) => {
if (!player.isVimeo) {
@@ -317,10 +308,10 @@ class Listeners {
}
const target = player.elements.wrapper.firstChild;
- const [, height] = ratio.split(':').map(Number);
- const [videoWidth, videoHeight] = player.embed.ratio.split(':').map(Number);
+ const [, y] = ratio;
+ const [videoX, videoY] = getAspectRatio.call(player);
- target.style.maxWidth = toggle ? `${(height / videoHeight) * videoWidth}px` : null;
+ target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
target.style.margin = toggle ? '0 auto' : null;
};
@@ -338,20 +329,24 @@ class Listeners {
};
const resized = () => {
- window.clearTimeout(timers.resized);
- timers.resized = window.setTimeout(setPlayerSize, 50);
+ clearTimeout(timers.resized);
+ timers.resized = setTimeout(setPlayerSize, 50);
};
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
const { target, usingNative } = player.fullscreen;
- // Ignore for iOS native
- if (!player.isEmbed || target !== elements.container) {
+ // Ignore events not from target
+ if (target !== elements.container) {
return;
}
- const isEnter = event.type === 'enterfullscreen';
+ // If it's not an embed and no ratio specified
+ if (!player.isEmbed && is.empty(player.config.ratio)) {
+ return;
+ }
+ const isEnter = event.type === 'enterfullscreen';
// Set the player size when entering fullscreen to viewport size
const { padding, ratio } = setPlayerSize(isEnter);
@@ -486,7 +481,7 @@ class Listeners {
// Update download link when ready and if quality changes
on.call(player, player.media, 'ready qualitychange', () => {
- controls.setDownloadLink.call(player);
+ controls.setDownloadUrl.call(player);
});
// Proxy events to container
@@ -518,7 +513,7 @@ class Listeners {
}
// Only call default handler if not prevented in custom handler
- if (returned && is.function(defaultHandler)) {
+ if (returned !== false && is.function(defaultHandler)) {
defaultHandler.call(player, event);
}
}
@@ -542,7 +537,6 @@ class Listeners {
controls() {
const { player } = this;
const { elements } = player;
-
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
@@ -678,7 +672,6 @@ class Listeners {
// Was playing before?
const play = seek.hasAttribute(attribute);
-
// Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
@@ -706,7 +699,6 @@ class Listeners {
inputEvent,
event => {
const seek = event.currentTarget;
-
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
@@ -806,7 +798,7 @@ class Listeners {
// Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin', () => {
- const { config, elements, timers } = player;
+ const { config, timers } = player;
// Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, true);
@@ -837,10 +829,8 @@ class Listeners {
// Detect "natural" scroll - suppored on OS X Safari only
// Other browsers on OS X will be inverted until support improves
const inverted = event.webkitDirectionInvertedFromDevice;
-
// Get delta from event. Invert if `inverted` is true
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
-
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js
index 6efd3295..db55e499 100644
--- a/src/js/plugins/ads.js
+++ b/src/js/plugins/ads.js
@@ -10,14 +10,28 @@ import { createElement } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import i18n from '../utils/i18n';
import is from '../utils/is';
-import loadScript from '../utils/loadScript';
+import loadScript from '../utils/load-script';
import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
+const destroy = instance => {
+ // Destroy our adsManager
+ if (instance.manager) {
+ instance.manager.destroy();
+ }
+
+ // Destroy our adsManager
+ if (instance.elements.displayContainer) {
+ instance.elements.displayContainer.destroy();
+ }
+
+ instance.elements.container.remove();
+};
+
class Ads {
/**
* Ads constructor.
- * @param {object} player
+ * @param {Object} player
* @return {Ads}
*/
constructor(player) {
@@ -63,20 +77,22 @@ class Ads {
* Load the IMA SDK
*/
load() {
- if (this.enabled) {
- // Check if the Google IMA3 SDK is loaded or load it ourselves
- if (!is.object(window.google) || !is.object(window.google.ima)) {
- loadScript(this.player.config.urls.googleIMA.sdk)
- .then(() => {
- this.ready();
- })
- .catch(() => {
- // Script failed to load or is blocked
- this.trigger('error', new Error('Google IMA SDK failed to load'));
- });
- } else {
- this.ready();
- }
+ if (!this.enabled) {
+ return;
+ }
+
+ // Check if the Google IMA3 SDK is loaded or load it ourselves
+ if (!is.object(window.google) || !is.object(window.google.ima)) {
+ loadScript(this.player.config.urls.googleIMA.sdk)
+ .then(() => {
+ this.ready();
+ })
+ .catch(() => {
+ // Script failed to load or is blocked
+ this.trigger('error', new Error('Google IMA SDK failed to load'));
+ });
+ } else {
+ this.ready();
}
}
@@ -84,6 +100,11 @@ class Ads {
* Get the ads instance ready
*/
ready() {
+ // Double check we're enabled
+ if (!this.enabled) {
+ destroy(this);
+ }
+
// Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()');
@@ -198,7 +219,7 @@ class Ads {
/**
* Update the ad countdown
- * @param {boolean} start
+ * @param {Boolean} start
*/
pollCountdown(start = false) {
if (!start) {
@@ -240,16 +261,13 @@ class Ads {
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints();
- // Set volume to match player
- this.manager.setVolume(this.player.volume);
-
// Add listeners to the required events
// Advertisement error events
this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
// Advertisement regular events
Object.keys(google.ima.AdEvent.Type).forEach(type => {
- this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event));
+ this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
});
// Resolve our adsManager
@@ -285,7 +303,6 @@ class Ads {
*/
onAdEvent(event) {
const { container } = this.player.elements;
-
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
// don't have ad object associated
const ad = event.getAd();
@@ -293,19 +310,18 @@ class Ads {
// Proxy event
const dispatchEvent = type => {
- const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
- triggerEvent.call(this.player, this.player.media, event);
+ triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
};
+ // Bubble the event
+ dispatchEvent(event.type);
+
switch (event.type) {
case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay
this.trigger('loaded');
- // Bubble event
- dispatchEvent(event.type);
-
// Start countdown
this.pollCountdown(true);
@@ -317,15 +333,19 @@ class Ads {
// console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
// console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
+
+ break;
+
+ case google.ima.AdEvent.Type.STARTED:
+ // Set volume to match player
+ this.manager.setVolume(this.player.volume);
+
break;
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played
- // Fire event
- dispatchEvent(event.type);
-
// TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done.
// Then we load new ads within a new adsManager. When the video
@@ -350,6 +370,7 @@ class Ads {
// playing when the IMA SDK is ready or has failed
this.loadAds();
+
break;
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
@@ -357,8 +378,6 @@ class Ads {
// for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content
- dispatchEvent(event.type);
-
this.pauseContent();
break;
@@ -369,26 +388,17 @@ class Ads {
// Fired when content should be resumed. This usually happens when an ad finishes
// or collapses
- dispatchEvent(event.type);
-
this.pollCountdown();
this.resumeContent();
break;
- case google.ima.AdEvent.Type.STARTED:
- case google.ima.AdEvent.Type.MIDPOINT:
- case google.ima.AdEvent.Type.COMPLETE:
- case google.ima.AdEvent.Type.IMPRESSION:
- case google.ima.AdEvent.Type.CLICK:
- dispatchEvent(event.type);
- break;
-
case google.ima.AdEvent.Type.LOG:
if (adData.adError) {
this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
}
+
break;
default:
@@ -463,6 +473,9 @@ class Ads {
// Play the requested advertisement whenever the adsManager is ready
this.managerPromise
.then(() => {
+ // Set volume to match player
+ this.manager.setVolume(this.player.volume);
+
// Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
@@ -559,7 +572,7 @@ class Ads {
/**
* Handles callbacks after an ad event was invoked
- * @param {string} event - Event type
+ * @param {String} event - Event type
*/
trigger(event, ...args) {
const handlers = this.events[event];
@@ -575,8 +588,8 @@ class Ads {
/**
* Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
+ * @param {String} event - Event type
+ * @param {Function} callback - Callback for when event occurs
* @return {Ads}
*/
on(event, callback) {
@@ -594,8 +607,8 @@ class Ads {
* The advertisement has 12 seconds to get its things together. We stop this timer when the
* advertisement is playing, or when a user action is required to start, then we clear the
* timer on ad ready
- * @param {number} time
- * @param {string} from
+ * @param {Number} time
+ * @param {String} from
*/
startSafetyTimer(time, from) {
this.player.debug.log(`Safety timer invoked from: ${from}`);
@@ -608,7 +621,7 @@ class Ads {
/**
* Clear our safety timer(s)
- * @param {string} from
+ * @param {String} from
*/
clearSafetyTimer(from) {
if (!is.nullOrUndefined(this.safetyTimer)) {
diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/preview-thumbnails.js
index 834d16f2..61021d64 100644
--- a/src/js/plugins/previewThumbnails.js
+++ b/src/js/plugins/preview-thumbnails.js
@@ -17,17 +17,17 @@ const parseVtt = vttDataString => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
- /([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
+ /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,
); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
if (matchTimes) {
result.startTime =
- Number(matchTimes[1]) * 60 * 60 +
+ Number(matchTimes[1] || 0) * 60 * 60 +
Number(matchTimes[2]) * 60 +
Number(matchTimes[3]) +
Number(`0.${matchTimes[4]}`);
result.endTime =
- Number(matchTimes[6]) * 60 * 60 +
+ Number(matchTimes[6] || 0) * 60 * 60 +
Number(matchTimes[7]) * 60 +
Number(matchTimes[8]) +
Number(`0.${matchTimes[9]}`);
@@ -100,6 +100,10 @@ class PreviewThumbnails {
}
this.getThumbnails().then(() => {
+ if (!this.enabled) {
+ return;
+ }
+
// Render DOM elements
this.render();
@@ -121,7 +125,6 @@ class PreviewThumbnails {
// If string, convert into single-element list
const urls = is.string(src) ? [src] : src;
-
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
const promises = urls.map(u => this.getThumbnail(u));
@@ -148,7 +151,12 @@ class PreviewThumbnails {
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
- if (!thumbnail.frames[0].text.startsWith('/')) {
+ // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
+ if (
+ !thumbnail.frames[0].text.startsWith('/') &&
+ !thumbnail.frames[0].text.startsWith('http://') &&
+ !thumbnail.frames[0].text.startsWith('https://')
+ ) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
@@ -220,6 +228,7 @@ class PreviewThumbnails {
// Only act on left mouse button (0), or touch device (event.button is false)
if (event.button === false || event.button === 0) {
this.mouseDown = true;
+
// Wait until media has a duration
if (this.player.media.duration) {
this.toggleScrubbingContainer(true);
@@ -293,7 +302,9 @@ class PreviewThumbnails {
this.elements.thumb.container.appendChild(timeContainer);
// Inject the whole thumb
- this.player.elements.progress.appendChild(this.elements.thumb.container);
+ if (is.element(this.player.elements.progress)) {
+ this.player.elements.progress.appendChild(this.elements.thumb.container);
+ }
// Create HTML element: plyr__preview-scrubbing-container
this.elements.scrubbing.container = createElement('div', {
@@ -307,7 +318,6 @@ class PreviewThumbnails {
if (this.mouseDown) {
this.setScrubbingContainerSize();
} else {
- this.toggleThumbContainer(true);
this.setThumbContainerSizeAndPos();
}
@@ -319,7 +329,10 @@ class PreviewThumbnails {
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
- this.toggleThumbContainer(hasThumb);
+ // Show the thumb container if we're not scrubbing
+ if (!this.mouseDown) {
+ this.toggleThumbContainer(hasThumb);
+ }
// No matching thumb found
if (!hasThumb) {
@@ -416,7 +429,9 @@ class PreviewThumbnails {
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
+ // eslint-disable-next-line no-param-reassign
image.dataset.deleting = true;
+
// This has to be set before the timeout - to prevent issues switching between hover and scrub
const { currentImageContainer } = this;
@@ -457,7 +472,6 @@ class PreviewThumbnails {
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
-
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
@@ -591,11 +605,9 @@ class PreviewThumbnails {
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
const plyrRect = this.player.elements.container.getBoundingClientRect();
const { container } = this.elements.thumb;
-
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
const minVal = plyrRect.left - seekbarRect.left + 10;
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
-
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
@@ -626,9 +638,13 @@ class PreviewThumbnails {
// Find difference between height and preview container height
const multiplier = this.thumbContainerHeight / frame.h;
+ // eslint-disable-next-line no-param-reassign
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
+ // eslint-disable-next-line no-param-reassign
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
+ // eslint-disable-next-line no-param-reassign
previewImage.style.left = `-${frame.x * multiplier}px`;
+ // eslint-disable-next-line no-param-reassign
previewImage.style.top = `-${frame.y * multiplier}px`;
}
}
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index a7664e73..91019abf 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -9,7 +9,7 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
-import loadScript from '../utils/loadScript';
+import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
@@ -48,14 +48,14 @@ const vimeo = {
// Set intial ratio
setAspectRatio.call(this);
- // Load the API if not already
+ // Load the SDK if not already
if (!is.object(window.Vimeo)) {
loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
.catch(error => {
- this.debug.warn('Vimeo API failed to load', error);
+ this.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(this);
@@ -91,7 +91,6 @@ const vimeo = {
}
const id = parseId(source);
-
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
@@ -102,7 +101,6 @@ const vimeo = {
// Get poster, if already set
const { poster } = player;
-
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
@@ -259,7 +257,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
- controls.setDownloadLink.call(player);
+ controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
@@ -281,8 +279,8 @@ const vimeo = {
// Set aspect ratio based on video size
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
const [width, height] = dimensions;
- player.embed.ratio = `${width}:${height}`;
- setAspectRatio.call(this, player.embed.ratio);
+ player.embed.ratio = [width, height];
+ setAspectRatio.call(this);
});
// Set autopause
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 0bd232e0..31d22bb4 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -7,8 +7,8 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
-import loadImage from '../utils/loadImage';
-import loadScript from '../utils/loadScript';
+import loadImage from '../utils/load-image';
+import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, generateId } from '../utils/strings';
import { setAspectRatio } from '../utils/style';
@@ -34,78 +34,78 @@ function assurePlaybackState(play) {
}
}
+function getHost(config) {
+ if (config.noCookie) {
+ return 'https://www.youtube-nocookie.com';
+ }
+
+ if (window.location.protocol === 'http:') {
+ return 'http://www.youtube.com';
+ }
+
+ // Use YouTube's default
+ return undefined;
+}
+
const youtube = {
setup() {
// Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
- // Set aspect ratio
- setAspectRatio.call(this);
-
// Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this);
} else {
- // Load the API
- loadScript(this.config.urls.youtube.sdk).catch(error => {
- this.debug.warn('YouTube API failed to load', error);
- });
-
- // Setup callback for the API
- // YouTube has it's own system of course...
- window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
-
- // Add to queue
- window.onYouTubeReadyCallbacks.push(() => {
- youtube.ready.call(this);
- });
+ // Reference current global callback
+ const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue
window.onYouTubeIframeAPIReady = () => {
- window.onYouTubeReadyCallbacks.forEach(callback => {
+ // Call global callback if set
+ if (is.function(callback)) {
callback();
- });
+ }
+
+ youtube.ready.call(this);
};
+
+ // Load the SDK
+ loadScript(this.config.urls.youtube.sdk).catch(error => {
+ this.debug.warn('YouTube API failed to load', error);
+ });
}
},
// Get the media title
getTitle(videoId) {
- // Try via undocumented API method first
- // This method disappears now and then though...
- // https://github.com/sampotts/plyr/issues/709
- if (is.function(this.embed.getVideoData)) {
- const { title } = this.embed.getVideoData();
-
- if (is.empty(title)) {
- this.config.title = title;
- ui.setTitle.call(this);
- return;
- }
- }
+ const url = format(this.config.urls.youtube.api, videoId);
- // Or via Google API
- const key = this.config.keys.google;
- if (is.string(key) && !is.empty(key)) {
- const url = format(this.config.urls.youtube.api, videoId, key);
+ fetch(url)
+ .then(data => {
+ if (is.object(data)) {
+ const { title, height, width } = data;
- fetch(url)
- .then(result => {
- if (is.object(result)) {
- this.config.title = result.items[0].snippet.title;
- ui.setTitle.call(this);
- }
- })
- .catch(() => {});
- }
+ // Set title
+ this.config.title = title;
+ ui.setTitle.call(this);
+
+ // Set aspect ratio
+ this.embed.ratio = [width, height];
+ }
+
+ setAspectRatio.call(this);
+ })
+ .catch(() => {
+ // Set aspect ratio
+ setAspectRatio.call(this);
+ });
},
// API ready
ready() {
const player = this;
-
// Ignore already setup (race condition)
- const currentId = player.media.getAttribute('id');
+ const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
@@ -121,25 +121,23 @@ const youtube = {
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
-
// Get poster, if already set
const { poster } = player;
-
// Replace media element
const container = createElement('div', { id, poster });
player.media = replaceElement(container, player.media);
// Id to poster wrapper
- const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`;
+ 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
.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))
- .then(posterSrc => {
+ .then(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
- if (!posterSrc.includes('maxres')) {
+ if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
@@ -151,7 +149,7 @@ const youtube = {
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
videoId,
- host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined,
+ host: getHost(config),
playerVars: extend(
{},
{
@@ -386,7 +384,7 @@ const youtube = {
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
- if (player.media.paused && !player.embed.hasPlayed) {
+ if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 90bf00c8..f30d334a 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
-// plyr.js v3.5.0-beta.5
+// plyr.js v3.5.6
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@@ -15,7 +15,7 @@ import Fullscreen from './fullscreen';
import Listeners from './listeners';
import media from './media';
import Ads from './plugins/ads';
-import PreviewThumbnails from './plugins/previewThumbnails';
+import PreviewThumbnails from './plugins/preview-thumbnails';
import source from './source';
import Storage from './storage';
import support from './support';
@@ -24,8 +24,10 @@ import { closest } from './utils/arrays';
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
-import loadSprite from './utils/loadSprite';
+import loadSprite from './utils/load-sprite';
+import { clamp } from './utils/numbers';
import { cloneDeep, extend } from './utils/objects';
+import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
import { parseUrl } from './utils/urls';
// Private properties
@@ -149,7 +151,6 @@ class Plyr {
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
const type = this.media.tagName.toLowerCase();
-
// Embed properties
let iframe = null;
let url = null;
@@ -301,8 +302,8 @@ class Plyr {
}
// Autoplay if required
- if (this.config.autoplay) {
- this.play();
+ if (this.isHTML5 && this.config.autoplay) {
+ setTimeout(() => this.play(), 10);
}
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -322,27 +323,27 @@ class Plyr {
* Types and provider helpers
*/
get isHTML5() {
- return Boolean(this.provider === providers.html5);
+ return this.provider === providers.html5;
}
get isEmbed() {
- return Boolean(this.isYouTube || this.isVimeo);
+ return this.isYouTube || this.isVimeo;
}
get isYouTube() {
- return Boolean(this.provider === providers.youtube);
+ return this.provider === providers.youtube;
}
get isVimeo() {
- return Boolean(this.provider === providers.vimeo);
+ return this.provider === providers.vimeo;
}
get isVideo() {
- return Boolean(this.type === types.video);
+ return this.type === types.video;
}
get isAudio() {
- return Boolean(this.type === types.audio);
+ return this.type === types.audio;
}
/**
@@ -403,7 +404,7 @@ class Plyr {
/**
* Toggle playback based on current status
- * @param {boolean} input
+ * @param {Boolean} input
*/
togglePlay(input) {
// Toggle based on current state if nothing passed
@@ -437,7 +438,7 @@ class Plyr {
/**
* Rewind
- * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
+ * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
*/
rewind(seekTime) {
this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
@@ -445,7 +446,7 @@ class Plyr {
/**
* Fast forward
- * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
+ * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
*/
forward(seekTime) {
this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
@@ -453,7 +454,7 @@ class Plyr {
/**
* Seek to a time
- * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
+ * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)
*/
set currentTime(input) {
// Bail if media duration isn't available yet
@@ -512,7 +513,6 @@ class Plyr {
get duration() {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
-
// Media duration can be NaN or Infinity before the media has loaded
const realDuration = (this.media || {}).duration;
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
@@ -523,7 +523,7 @@ class Plyr {
/**
* Set the player volume
- * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
+ * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
*/
set volume(value) {
let volume = value;
@@ -574,7 +574,7 @@ class Plyr {
/**
* Increase volume
- * @param {boolean} step - How much to decrease by (between 0 and 1)
+ * @param {Boolean} step - How much to decrease by (between 0 and 1)
*/
increaseVolume(step) {
const volume = this.media.muted ? 0 : this.volume;
@@ -583,7 +583,7 @@ class Plyr {
/**
* Decrease volume
- * @param {boolean} step - How much to decrease by (between 0 and 1)
+ * @param {Boolean} step - How much to decrease by (between 0 and 1)
*/
decreaseVolume(step) {
this.increaseVolume(-step);
@@ -591,7 +591,7 @@ class Plyr {
/**
* Set muted state
- * @param {boolean} mute
+ * @param {Boolean} mute
*/
set muted(mute) {
let toggle = mute;
@@ -643,7 +643,7 @@ class Plyr {
/**
* Set playback speed
- * @param {number} speed - the speed of playback (0.5-2.0)
+ * @param {Number} speed - the speed of playback (0.5-2.0)
*/
set speed(input) {
let speed = null;
@@ -660,24 +660,17 @@ class Plyr {
speed = this.config.speed.selected;
}
- // Set min/max
- if (speed < 0.1) {
- speed = 0.1;
- }
- if (speed > 2.0) {
- speed = 2.0;
- }
-
- if (!this.config.speed.options.includes(speed)) {
- this.debug.warn(`Unsupported speed (${speed})`);
- return;
- }
+ // Clamp to min/max
+ const { minimumSpeed: min, maximumSpeed: max } = this;
+ speed = clamp(speed, min, max);
// Update config
this.config.speed.selected = speed;
// Set media speed
- this.media.playbackRate = speed;
+ setTimeout(() => {
+ this.media.playbackRate = speed;
+ }, 0);
}
/**
@@ -688,9 +681,45 @@ class Plyr {
}
/**
+ * Get the minimum allowed speed
+ */
+ get minimumSpeed() {
+ if (this.isYouTube) {
+ // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
+ return Math.min(...this.options.speed);
+ }
+
+ if (this.isVimeo) {
+ // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
+ return 0.5;
+ }
+
+ // https://stackoverflow.com/a/32320020/1191319
+ return 0.0625;
+ }
+
+ /**
+ * Get the maximum allowed speed
+ */
+ get maximumSpeed() {
+ if (this.isYouTube) {
+ // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
+ return Math.max(...this.options.speed);
+ }
+
+ if (this.isVimeo) {
+ // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
+ return 2;
+ }
+
+ // https://stackoverflow.com/a/32320020/1191319
+ return 16;
+ }
+
+ /**
* Set playback quality
* Currently HTML5 & YouTube only
- * @param {number} input - Quality level
+ * @param {Number} input - Quality level
*/
set quality(input) {
const config = this.config.quality;
@@ -740,7 +769,7 @@ class Plyr {
/**
* Toggle loop
* TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
- * @param {boolean} input - Whether to loop or not
+ * @param {Boolean} input - Whether to loop or not
*/
set loop(input) {
const toggle = is.boolean(input) ? input : this.config.loop.active;
@@ -800,7 +829,7 @@ class Plyr {
/**
* Set new media source
- * @param {object} input - The new source object (see docs)
+ * @param {Object} input - The new source object (see docs)
*/
set source(input) {
source.change.call(this, input);
@@ -823,8 +852,21 @@ class Plyr {
}
/**
+ * Set the download URL
+ */
+ set download(input) {
+ if (!is.url(input)) {
+ return;
+ }
+
+ this.config.urls.download = input;
+
+ controls.setDownloadUrl.call(this);
+ }
+
+ /**
* Set the poster image for a video
- * @param {input} - the URL for the new poster image
+ * @param {String} input - the URL for the new poster image
*/
set poster(input) {
if (!this.isVideo) {
@@ -847,8 +889,40 @@ class Plyr {
}
/**
+ * Get the current aspect ratio in use
+ */
+ get ratio() {
+ if (!this.isVideo) {
+ return null;
+ }
+
+ const ratio = reduceAspectRatio(getAspectRatio.call(this));
+
+ return is.array(ratio) ? ratio.join(':') : ratio;
+ }
+
+ /**
+ * Set video aspect ratio
+ */
+ set ratio(input) {
+ if (!this.isVideo) {
+ this.debug.warn('Aspect ratio can only be set for video');
+ return;
+ }
+
+ if (!is.string(input) || !validateRatio(input)) {
+ this.debug.error(`Invalid aspect ratio specified (${input})`);
+ return;
+ }
+
+ this.config.ratio = input;
+
+ setAspectRatio.call(this);
+ }
+
+ /**
* Set the autoplay state
- * @param {boolean} input - Whether to autoplay or not
+ * @param {Boolean} input - Whether to autoplay or not
*/
set autoplay(input) {
const toggle = is.boolean(input) ? input : this.config.autoplay;
@@ -864,7 +938,7 @@ class Plyr {
/**
* Toggle captions
- * @param {boolean} input - Whether to enable captions
+ * @param {Boolean} input - Whether to enable captions
*/
toggleCaptions(input) {
captions.toggle.call(this, input, false);
@@ -872,7 +946,7 @@ class Plyr {
/**
* Set the caption track by index
- * @param {number} - Caption index
+ * @param {Number} - Caption index
*/
set currentTrack(input) {
captions.set.call(this, input, false);
@@ -889,7 +963,7 @@ class Plyr {
/**
* Set the wanted language for captions
* Since tracks can be added later it won't update the actual caption track until there is a matching track
- * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
+ * @param {String} - Two character ISO language code (e.g. EN, FR, PT, etc)
*/
set language(input) {
captions.setLanguage.call(this, input, false);
@@ -962,17 +1036,15 @@ class Plyr {
/**
* Toggle the player controls
- * @param {boolean} [toggle] - Whether to show the controls
+ * @param {Boolean} [toggle] - Whether to show the controls
*/
toggleControls(toggle) {
// Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
-
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
-
// Apply and get updated state
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
@@ -995,8 +1067,8 @@ class Plyr {
/**
* Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
+ * @param {String} event - Event type
+ * @param {Function} callback - Callback for when event occurs
*/
on(event, callback) {
on.call(this, this.elements.container, event, callback);
@@ -1004,8 +1076,8 @@ class Plyr {
/**
* Add event listeners once
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
+ * @param {String} event - Event type
+ * @param {Function} callback - Callback for when event occurs
*/
once(event, callback) {
once.call(this, this.elements.container, event, callback);
@@ -1013,8 +1085,8 @@ class Plyr {
/**
* Remove event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
+ * @param {String} event - Event type
+ * @param {Function} callback - Callback for when event occurs
*/
off(event, callback) {
off(this.elements.container, event, callback);
@@ -1024,8 +1096,8 @@ class Plyr {
* Destroy an instance
* Event listeners are removed when elements are removed
* http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
- * @param {function} callback - Callback for when destroy is complete
- * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
+ * @param {Function} callback - Callback for when destroy is complete
+ * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
*/
destroy(callback, soft = false) {
if (!this.ready) {
@@ -1088,11 +1160,13 @@ class Plyr {
// Stop playback
this.stop();
+ // Clear timeouts
+ clearTimeout(this.timers.loading);
+ clearTimeout(this.timers.controls);
+ clearTimeout(this.timers.resized);
+
// Provider specific stuff
if (this.isHTML5) {
- // Clear timeout
- clearTimeout(this.timers.loading);
-
// Restore native video controls
ui.toggleNativeControls.call(this, true);
@@ -1124,7 +1198,7 @@ class Plyr {
/**
* Check for support for a mime type (HTML5 only)
- * @param {string} type - Mime type
+ * @param {String} type - Mime type
*/
supports(type) {
return support.mime.call(this, type);
@@ -1132,9 +1206,9 @@ class Plyr {
/**
* Check for support
- * @param {string} type - Player type (audio/video)
- * @param {string} provider - Provider (html5/youtube/vimeo)
- * @param {bool} inline - Where player has `playsinline` sttribute
+ * @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);
@@ -1142,8 +1216,8 @@ class Plyr {
/**
* Load an SVG sprite into the page
- * @param {string} url - URL for the SVG sprite
- * @param {string} [id] - Unique ID
+ * @param {String} url - URL for the SVG sprite
+ * @param {String} [id] - Unique ID
*/
static loadSprite(url, id) {
return loadSprite(url, id);
@@ -1152,7 +1226,7 @@ class Plyr {
/**
* Setup multiple instances
* @param {*} selector
- * @param {object} options
+ * @param {Object} options
*/
static setup(selector, options = {}) {
let targets = null;
diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js
index 92af952e..8a1cb7a1 100644
--- a/src/js/plyr.polyfilled.js
+++ b/src/js/plyr.polyfilled.js
@@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
-// plyr.js v3.5.0-beta.5
+// plyr.js v3.5.6
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'custom-event-polyfill';
import 'url-polyfill';
+
import Plyr from './plyr';
export default Plyr;
diff --git a/src/js/ui.js b/src/js/ui.js
index 8e50bb83..953ecba2 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
-import loadImage from './utils/loadImage';
+import loadImage from './utils/load-image';
const ui = {
addStyleHook() {
@@ -67,15 +67,15 @@ const ui = {
// Reset mute state
this.muted = null;
- // Reset speed
- this.speed = null;
-
// Reset loop state
this.loop = null;
// Reset quality setting
this.quality = null;
+ // Reset speed
+ this.speed = null;
+
// Reset volume display
controls.updateVolume.call(this);
@@ -213,7 +213,7 @@ const ui = {
// Set state
Array.from(this.elements.buttons.play || []).forEach(target => {
- target.pressed = this.playing;
+ Object.assign(target, { pressed: this.playing });
});
// Only update controls on non timeupdate events
@@ -233,25 +233,37 @@ const ui = {
clearTimeout(this.timers.loading);
// Timer to prevent flicker when seeking
- this.timers.loading = setTimeout(() => {
- // Update progress bar loading class state
- toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
-
- // Update controls visibility
- ui.toggleControls.call(this);
- }, this.loading ? 250 : 0);
+ this.timers.loading = setTimeout(
+ () => {
+ // Update progress bar loading class state
+ toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
+
+ // Update controls visibility
+ ui.toggleControls.call(this);
+ },
+ this.loading ? 250 : 0,
+ );
},
// Toggle controls based on state and `force` argument
toggleControls(force) {
- const { controls } = this.elements;
+ const { controls: controlsElement } = this.elements;
- if (controls && this.config.hideControls) {
+ if (controlsElement && this.config.hideControls) {
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
- const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
+ const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
- this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
+ this.toggleControls(
+ Boolean(
+ force ||
+ this.loading ||
+ this.paused ||
+ controlsElement.pressed ||
+ controlsElement.hover ||
+ recentTouchSeek,
+ ),
+ );
}
},
};
diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js
index 6b950b61..3f721b5a 100644
--- a/src/js/utils/animation.js
+++ b/src/js/utils/animation.js
@@ -2,7 +2,6 @@
// Animation utils
// ==========================================================================
-import { toggleHidden } from './elements';
import is from './is';
export const transitionEndEvent = (() => {
@@ -21,14 +20,19 @@ export const transitionEndEvent = (() => {
})();
// Force repaint of element
-export function repaint(element) {
+export function repaint(element, delay) {
setTimeout(() => {
try {
- toggleHidden(element, true);
- element.offsetHeight; // eslint-disable-line
- toggleHidden(element, false);
+ // eslint-disable-next-line no-param-reassign
+ element.hidden = true;
+
+ // eslint-disable-next-line no-unused-expressions
+ element.offsetHeight;
+
+ // eslint-disable-next-line no-param-reassign
+ element.hidden = false;
} catch (e) {
// Do nothing
}
- }, 0);
+ }, delay);
}
diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js
index 6be634e5..4f10938e 100644
--- a/src/js/utils/elements.js
+++ b/src/js/utils/elements.js
@@ -4,6 +4,7 @@
import { toggleListener } from './events';
import is from './is';
+import { extend } from './objects';
// Wrap an element
export function wrap(elements, wrapper) {
@@ -16,7 +17,6 @@ export function wrap(elements, wrapper) {
.reverse()
.forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
-
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
@@ -137,30 +137,28 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
const attributes = {};
- const existing = existingAttributes;
+ const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
-
// Get the parts and value
const parts = stripped.split('=');
- const key = parts[0];
+ const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
-
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
- if (is.object(existing) && is.string(existing.class)) {
- existing.class += ` ${className}`;
+ if (is.string(existing.class)) {
+ attributes.class = `${existing.class} ${className}`;
+ } else {
+ attributes.class = className;
}
-
- attributes.class = className;
break;
case '#':
@@ -179,7 +177,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
});
- return attributes;
+ return extend(existing, attributes);
}
// Toggle hidden
@@ -194,11 +192,8 @@ export function toggleHidden(element, hidden) {
hide = !element.hidden;
}
- if (hide) {
- element.setAttribute('hidden', '');
- } else {
- element.removeAttribute('hidden');
- }
+ // eslint-disable-next-line no-param-reassign
+ element.hidden = hide;
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
@@ -233,14 +228,14 @@ export function matches(element, selector) {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
- const matches =
+ const method =
prototype.matches ||
prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector ||
prototype.msMatchesSelector ||
match;
- return matches.call(element, selector);
+ return method.call(element, selector);
}
// Find all elements
diff --git a/src/js/utils/events.js b/src/js/utils/events.js
index d304c312..87c35d26 100644
--- a/src/js/utils/events.js
+++ b/src/js/utils/events.js
@@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive
// Allow multiple events
const events = event.split(' ');
-
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
diff --git a/src/js/utils/i18n.js b/src/js/utils/i18n.js
index 758ed695..5eee5829 100644
--- a/src/js/utils/i18n.js
+++ b/src/js/utils/i18n.js
@@ -36,8 +36,8 @@ const i18n = {
'{title}': config.title,
};
- Object.entries(replace).forEach(([key, value]) => {
- string = replaceAll(string, key, value);
+ Object.entries(replace).forEach(([k, v]) => {
+ string = replaceAll(string, k, v);
});
return string;
diff --git a/src/js/utils/loadImage.js b/src/js/utils/load-image.js
index 8acd2496..8acd2496 100644
--- a/src/js/utils/loadImage.js
+++ b/src/js/utils/load-image.js
diff --git a/src/js/utils/loadScript.js b/src/js/utils/load-script.js
index 81ae36f4..81ae36f4 100644
--- a/src/js/utils/loadScript.js
+++ b/src/js/utils/load-script.js
diff --git a/src/js/utils/loadSprite.js b/src/js/utils/load-sprite.js
index 917bd6ac..fe4add00 100644
--- a/src/js/utils/loadSprite.js
+++ b/src/js/utils/load-sprite.js
@@ -15,10 +15,10 @@ export default function loadSprite(url, id) {
const prefix = 'cache';
const hasId = is.string(id);
let isCached = false;
-
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => {
+ // eslint-disable-next-line no-param-reassign
container.innerHTML = data;
// Check again incase of race condition
@@ -33,7 +33,6 @@ export default function loadSprite(url, id) {
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
-
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
diff --git a/src/js/utils/numbers.js b/src/js/utils/numbers.js
new file mode 100644
index 00000000..f6eb65c8
--- /dev/null
+++ b/src/js/utils/numbers.js
@@ -0,0 +1,17 @@
+/**
+ * Returns a number whose value is limited to the given range.
+ *
+ * Example: limit the output of this computation to between 0 and 255
+ * (x * 255).clamp(0, 255)
+ *
+ * @param {Number} input
+ * @param {Number} min The lower boundary of the output range
+ * @param {Number} max The upper boundary of the output range
+ * @returns A number in the range [min, max]
+ * @type Number
+ */
+export function clamp(input = 0, min = 0, max = 255) {
+ return Math.min(Math.max(input, min), max);
+}
+
+export default { clamp };
diff --git a/src/js/utils/style.js b/src/js/utils/style.js
index a8eb393b..941db8f2 100644
--- a/src/js/utils/style.js
+++ b/src/js/utils/style.js
@@ -4,26 +4,61 @@
import is from './is';
-/* function reduceAspectRatio(width, height) {
- const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
- const ratio = getRatio(width, height);
- return `${width / ratio}:${height / ratio}`;
-} */
+export function validateRatio(input) {
+ if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
+ return false;
+ }
-// Set aspect ratio for responsive container
-export function setAspectRatio(input) {
- let ratio = input;
+ const ratio = is.array(input) ? input : input.split(':');
+
+ return ratio.map(Number).every(is.number);
+}
+
+export function reduceAspectRatio(ratio) {
+ if (!is.array(ratio) || !ratio.every(is.number)) {
+ return null;
+ }
+
+ const [width, height] = ratio;
+ const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));
+ const divider = getDivider(width, height);
+
+ return [width / divider, height / divider];
+}
- if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) {
+export function getAspectRatio(input) {
+ const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
+ // Try provided ratio
+ let ratio = parse(input);
+
+ // Get from config
+ if (ratio === null) {
+ ratio = parse(this.config.ratio);
+ }
+
+ // Get from embed
+ if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
({ ratio } = this.embed);
}
- if (!is.string(ratio)) {
- ({ ratio } = this.config);
+ // Get from HTML5 video
+ if (ratio === null && this.isHTML5) {
+ const { videoWidth, videoHeight } = this.media;
+ ratio = reduceAspectRatio([videoWidth, videoHeight]);
+ }
+
+ return ratio;
+}
+
+// Set aspect ratio for responsive container
+export function setAspectRatio(input) {
+ if (!this.isVideo) {
+ return {};
}
- const [x, y] = ratio.split(':').map(Number);
- const padding = (100 / x) * y;
+ const ratio = getAspectRatio.call(this, input);
+ const [w, h] = is.array(ratio) ? ratio : [0, 0];
+ const padding = (100 / w) * h;
this.elements.wrapper.style.paddingBottom = `${padding}%`;
@@ -32,6 +67,8 @@ export function setAspectRatio(input) {
const height = 240;
const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
+ } else if (this.isHTML5) {
+ this.elements.wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
return { padding, ratio };
diff --git a/src/js/utils/time.js b/src/js/utils/time.js
index 2deccf65..ffca88b2 100644
--- a/src/js/utils/time.js
+++ b/src/js/utils/time.js
@@ -18,7 +18,6 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
// Format time component to add leading zero
const format = value => `0${value}`.slice(-2);
-
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
diff --git a/src/js/utils/urls.js b/src/js/utils/urls.js
index 3ebe622e..843c6aa6 100644
--- a/src/js/utils/urls.js
+++ b/src/js/utils/urls.js
@@ -6,8 +6,8 @@ import is from './is';
/**
* Parse a string to a URL object
- * @param {string} input - the URL to be parsed
- * @param {boolean} safe - failsafe parsing
+ * @param {String} input - the URL to be parsed
+ * @param {Boolean} safe - failsafe parsing
*/
export function parseUrl(input, safe = true) {
let url = input;
diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss
index 1c9aab2b..0022d17b 100644
--- a/src/sass/components/control.scss
+++ b/src/sass/components/control.scss
@@ -63,10 +63,6 @@ a.plyr__control {
// Video control
.plyr--video .plyr__control {
- svg {
- filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
- }
-
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
@@ -81,7 +77,6 @@ a.plyr__control {
background: rgba($plyr-video-control-bg-hover, 0.8);
border: 0;
border-radius: 100%;
- box-shadow: 0 1px 1px rgba(#000, 0.15);
color: $plyr-video-control-color;
display: none;
left: 50%;
diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss
index 41426e8b..8abee204 100644
--- a/src/sass/components/controls.scss
+++ b/src/sass/components/controls.scss
@@ -14,42 +14,47 @@
justify-content: flex-end;
text-align: center;
- // Spacing
- > .plyr__control,
- .plyr__progress,
- .plyr__time,
- .plyr__menu,
- .plyr__volume {
- margin-left: ($plyr-control-spacing / 2);
+ .plyr__progress__container {
+ flex: 1;
+ min-width: 0; // Fix for Edge issue where content would overflow
}
- .plyr__menu + .plyr__control,
- > .plyr__control + .plyr__menu,
- > .plyr__control + .plyr__control,
- .plyr__progress + .plyr__control {
- margin-left: floor($plyr-control-spacing / 4);
- }
+ // Spacing
+ .plyr__controls__item {
+ margin-left: ($plyr-control-spacing / 4);
+
+ &:first-child {
+ margin-left: 0;
+ margin-right: auto;
+ }
+
+ &.plyr__progress__container {
+ padding-left: ($plyr-control-spacing / 4);
+ }
+
+ &.plyr__time {
+ padding: 0 ($plyr-control-spacing / 2);
+ }
- > .plyr__control:first-child,
- > .plyr__control:first-child + [data-plyr='pause'] {
- margin-left: 0;
- margin-right: auto;
+ &.plyr__progress__container:first-child,
+ &.plyr__time:first-child,
+ &.plyr__time + .plyr__time {
+ padding-left: 0;
+ }
+
+ &.plyr__volume {
+ padding-right: ($plyr-control-spacing / 2);
+ }
+
+ &.plyr__volume:first-child {
+ padding-right: 0;
+ }
}
// Hide empty controls
&:empty {
display: none;
}
-
- @media (min-width: $plyr-bp-sm) {
- > .plyr__control,
- .plyr__menu,
- .plyr__progress,
- .plyr__time,
- .plyr__volume {
- margin-left: $plyr-control-spacing;
- }
- }
}
// Audio controls
@@ -62,10 +67,7 @@
// Video controls
.plyr--video .plyr__controls {
- background: linear-gradient(
- rgba($plyr-video-controls-bg, 0),
- rgba($plyr-video-controls-bg, 0.7)
- );
+ background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
bottom: 0;
diff --git a/src/sass/components/embed.scss b/src/sass/components/embed.scss
deleted file mode 100644
index 25431caf..00000000
--- a/src/sass/components/embed.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-// --------------------------------------------------------------
-// Embedded players
-// YouTube, Vimeo, etc
-// --------------------------------------------------------------
-
-// Default to 16:9 ratio but this is set by JavaScript based on config
-$embed-padding: ((100 / 16) * 9);
-
-.plyr__video-embed {
- height: 0;
- padding-bottom: to-percentage($embed-padding);
- position: relative;
-
- iframe {
- border: 0;
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- user-select: none;
- width: 100%;
- }
-}
-
-// If the full custom UI is supported
-.plyr--full-ui .plyr__video-embed {
- $height: 240;
- $offset: to-percentage(($height - $embed-padding) / ($height / 50));
-
- // Only used for Vimeo
- > .plyr__video-embed__container {
- padding-bottom: to-percentage($height);
- position: relative;
- transform: translateY(-$offset);
- }
-}
diff --git a/src/sass/components/progress.scss b/src/sass/components/progress.scss
index f28a19ca..04c83516 100644
--- a/src/sass/components/progress.scss
+++ b/src/sass/components/progress.scss
@@ -2,18 +2,19 @@
// Playback progress
// --------------------------------------------------------------
+// Offset the range thumb in order to be able to calculate the relative progress (#954)
+$plyr-progress-offset: $plyr-range-thumb-height;
+
.plyr__progress {
- flex: 1;
- left: $plyr-range-thumb-height / 2;
- margin-right: $plyr-range-thumb-height;
+ left: $plyr-progress-offset / 2;
+ margin-right: $plyr-progress-offset;
position: relative;
input[type='range'],
&__buffer {
- margin-left: -($plyr-range-thumb-height / 2);
- margin-right: -($plyr-range-thumb-height / 2);
- // Offset the range thumb in order to be able to calculate the relative progress (#954)
- width: calc(100% + #{$plyr-range-thumb-height});
+ margin-left: -($plyr-progress-offset / 2);
+ margin-right: -($plyr-progress-offset / 2);
+ width: calc(100% + #{$plyr-progress-offset});
}
input[type='range'] {
diff --git a/src/sass/components/video.scss b/src/sass/components/video.scss
index c3dc4152..fdcf4f2d 100644
--- a/src/sass/components/video.scss
+++ b/src/sass/components/video.scss
@@ -20,3 +20,36 @@
// Require z-index to force border-radius
z-index: 0;
}
+
+// Default to 16:9 ratio but this is set by JavaScript based on config
+$embed-padding: ((100 / 16) * 9);
+
+.plyr__video-embed,
+.plyr__video-wrapper--fixed-ratio {
+ height: 0;
+ padding-bottom: to-percentage($embed-padding);
+}
+
+.plyr__video-embed iframe,
+.plyr__video-wrapper--fixed-ratio video {
+ border: 0;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ user-select: none;
+ width: 100%;
+}
+
+// If the full custom UI is supported
+.plyr--full-ui .plyr__video-embed {
+ $height: 240;
+ $offset: to-percentage(($height - $embed-padding) / ($height / 50));
+
+ // Only used for Vimeo
+ > .plyr__video-embed__container {
+ padding-bottom: to-percentage($height);
+ position: relative;
+ transform: translateY(-$offset);
+ }
+}
diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss
index 554c66a5..5a1ca753 100644
--- a/src/sass/lib/mixins.scss
+++ b/src/sass/lib/mixins.scss
@@ -62,12 +62,13 @@
.plyr__video-wrapper {
height: 100%;
- width: 100%;
+ position: static;
}
// Vimeo requires some different styling
&.plyr--vimeo .plyr__video-wrapper {
height: 0;
+ position: relative;
top: 50%;
transform: translateY(-50%);
}
diff --git a/src/sass/plugins/ads.scss b/src/sass/plugins/ads.scss
index c5acef75..44ec5351 100644
--- a/src/sass/plugins/ads.scss
+++ b/src/sass/plugins/ads.scss
@@ -23,7 +23,7 @@
// The countdown label
&::after {
- background: rgba($plyr-color-gunmetal, 0.8);
+ background: rgba($plyr-color-gray-9, 0.8);
border-radius: 2px;
bottom: $plyr-control-spacing;
color: #fff;
diff --git a/src/sass/plugins/previewThumbnails.scss b/src/sass/plugins/preview-thumbnails.scss
index 02a2f619..b2b272c1 100644
--- a/src/sass/plugins/previewThumbnails.scss
+++ b/src/sass/plugins/preview-thumbnails.scss
@@ -7,7 +7,7 @@ $plyr-preview-bg: $plyr-tooltip-bg !default;
$plyr-preview-radius: $plyr-tooltip-radius !default;
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
-$plyr-preview-image-bg: $plyr-color-heather !default;
+$plyr-preview-image-bg: $plyr-color-gray-2 !default;
$plyr-preview-time-font-size: $plyr-font-size-time !default;
$plyr-preview-time-padding: 3px 6px !default;
$plyr-preview-time-bg: rgba(0, 0, 0, 0.55);
diff --git a/src/sass/plyr.scss b/src/sass/plyr.scss
index 468c534c..144297f7 100644
--- a/src/sass/plyr.scss
+++ b/src/sass/plyr.scss
@@ -29,7 +29,6 @@
@import 'components/captions';
@import 'components/control';
@import 'components/controls';
-@import 'components/embed';
@import 'components/menus';
@import 'components/sliders';
@import 'components/poster';
@@ -42,7 +41,7 @@
@import 'states/fullscreen';
@import 'plugins/ads';
-@import 'plugins/previewThumbnails';
+@import 'plugins/preview-thumbnails';
@import 'utils/animation';
@import 'utils/hidden';
diff --git a/src/sass/settings/badges.scss b/src/sass/settings/badges.scss
index 4f98c9a8..5fd0c138 100644
--- a/src/sass/settings/badges.scss
+++ b/src/sass/settings/badges.scss
@@ -2,5 +2,5 @@
// Badges
// ==========================================================================
-$plyr-badge-bg: $plyr-color-fiord !default;
+$plyr-badge-bg: $plyr-color-gray-7 !default;
$plyr-badge-color: #fff !default;
diff --git a/src/sass/settings/colors.scss b/src/sass/settings/colors.scss
index c9ea580c..e3883eef 100644
--- a/src/sass/settings/colors.scss
+++ b/src/sass/settings/colors.scss
@@ -2,8 +2,16 @@
// Colors
// ==========================================================================
-$plyr-color-main: #1aafff !default;
-$plyr-color-gunmetal: #2f343d !default;
-$plyr-color-fiord: #4f5b5f !default;
-$plyr-color-lynch: #6b7d85 !default;
-$plyr-color-heather: #b7c5cd !default;
+$plyr-color-main: hsl(198, 100%, 50%) !default;
+
+// Grayscale
+$plyr-color-gray-9: hsl(210, 15%, 16%);
+$plyr-color-gray-8: lighten($plyr-color-gray-9, 9%);
+$plyr-color-gray-7: lighten($plyr-color-gray-8, 9%);
+$plyr-color-gray-6: lighten($plyr-color-gray-7, 9%);
+$plyr-color-gray-5: lighten($plyr-color-gray-6, 9%);
+$plyr-color-gray-4: lighten($plyr-color-gray-5, 9%);
+$plyr-color-gray-3: lighten($plyr-color-gray-4, 9%);
+$plyr-color-gray-2: lighten($plyr-color-gray-3, 9%);
+$plyr-color-gray-1: lighten($plyr-color-gray-2, 9%);
+$plyr-color-gray-0: lighten($plyr-color-gray-1, 9%);
diff --git a/src/sass/settings/controls.scss b/src/sass/settings/controls.scss
index d6d2c153..da9f4e58 100644
--- a/src/sass/settings/controls.scss
+++ b/src/sass/settings/controls.scss
@@ -13,6 +13,6 @@ $plyr-video-control-color-hover: #fff !default;
$plyr-video-control-bg-hover: $plyr-color-main !default;
$plyr-audio-controls-bg: #fff !default;
-$plyr-audio-control-color: $plyr-color-fiord !default;
+$plyr-audio-control-color: $plyr-color-gray-7 !default;
$plyr-audio-control-color-hover: #fff !default;
$plyr-audio-control-bg-hover: $plyr-color-main !default;
diff --git a/src/sass/settings/menus.scss b/src/sass/settings/menus.scss
index 64df9863..420ebb03 100644
--- a/src/sass/settings/menus.scss
+++ b/src/sass/settings/menus.scss
@@ -3,8 +3,8 @@
// ==========================================================================
$plyr-menu-bg: rgba(#fff, 0.9) !default;
-$plyr-menu-color: $plyr-color-fiord !default;
+$plyr-menu-color: $plyr-color-gray-7 !default;
$plyr-menu-arrow-size: 6px !default;
-$plyr-menu-border-color: $plyr-color-heather !default;
+$plyr-menu-border-color: $plyr-color-gray-2 !default;
$plyr-menu-border-shadow-color: #fff !default;
$plyr-menu-shadow: 0 1px 2px rgba(#000, 0.15) !default;
diff --git a/src/sass/settings/progress.scss b/src/sass/settings/progress.scss
index 074ee3c6..10b6ebb7 100644
--- a/src/sass/settings/progress.scss
+++ b/src/sass/settings/progress.scss
@@ -4,8 +4,8 @@
// Loading
$plyr-progress-loading-size: 25px !default;
-$plyr-progress-loading-bg: rgba($plyr-color-gunmetal, 0.6) !default;
+$plyr-progress-loading-bg: rgba($plyr-color-gray-9, 0.6) !default;
// Buffered
$plyr-video-progress-buffered-bg: rgba(#fff, 0.25) !default;
-$plyr-audio-progress-buffered-bg: rgba($plyr-color-heather, 0.66) !default;
+$plyr-audio-progress-buffered-bg: rgba($plyr-color-gray-2, 0.66) !default;
diff --git a/src/sass/settings/sliders.scss b/src/sass/settings/sliders.scss
index 6ac053b0..c4d239ae 100644
--- a/src/sass/settings/sliders.scss
+++ b/src/sass/settings/sliders.scss
@@ -9,7 +9,7 @@ $plyr-range-thumb-active-shadow-width: 3px !default;
$plyr-range-thumb-height: 13px !default;
$plyr-range-thumb-bg: #fff !default;
$plyr-range-thumb-border: 2px solid transparent !default;
-$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default;
+$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gray-9, 0.2) !default;
// Track
$plyr-range-track-height: 5px !default;
diff --git a/src/sass/settings/tooltips.scss b/src/sass/settings/tooltips.scss
index fd304d60..2d298ef8 100644
--- a/src/sass/settings/tooltips.scss
+++ b/src/sass/settings/tooltips.scss
@@ -3,7 +3,7 @@
// ==========================================================================
$plyr-tooltip-bg: rgba(#fff, 0.9) !default;
-$plyr-tooltip-color: $plyr-color-fiord !default;
+$plyr-tooltip-color: $plyr-color-gray-7 !default;
$plyr-tooltip-padding: ($plyr-control-spacing / 2) !default;
$plyr-tooltip-arrow-size: 4px !default;
$plyr-tooltip-radius: 3px !default;