diff --git a/.eslintrc.json b/.eslintrc
similarity index 100%
rename from .eslintrc.json
rename to .eslintrc
diff --git a/changelog.md b/changelog.md
index 8b3d378fb..f3284a08e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,16 @@
+## v3.5.4
+
+- Added: Set download URL via new setter
+- Improvement: The order of the `controls` option now effects the order in the DOM - i.e. you can re-order the controls - Note: this may break any custom CSS you have setup. Please see the changes in the PR to the default SASS
+- Fixed issue with empty controls and preview thumbs
+- Fixed issue with setGutter call (from Sentry)
+- Fixed issue with initial selected speed not working
+- Added notes on `autoplay` config option and browser compatibility
+- Fixed issue with ads volume not matching current content volume
+- Fixed race condition where ads were loading during source change
+- Improvement: Automatic aspect ratio for YouTube is now supported, meaning all aspect ratios are set based on media content - Note: we're now using a different API to get YouTube video metadata so you may need to adjust any CSPs you have setup
+- Fix for menu in the Shadow DOM (thanks @emielbeinema)
+
## v3.5.3
- Improved the usage of the `ratio` config option; it now works as expected and for all video types. The default has not changed, it is to dynamically, where possible (except YouTube where 16:9 is used) determine the ratio from the media source so this is not a breaking change.
diff --git a/demo/index.html b/demo/index.html
index c1942e74b..04458522f 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -265,7 +265,7 @@
Plyr
diff --git a/package.json b/package.json
index ddc4bf4ff..d4a327a1e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "plyr",
- "version": "3.5.3",
+ "version": "3.5.4",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"author": "Sam Potts ",
@@ -78,7 +78,7 @@
"stylelint": "^9.10.1",
"stylelint-config-prettier": "^5.0.0",
"stylelint-config-recommended": "^2.1.0",
- "stylelint-config-sass-guidelines": "^5.3.0",
+ "stylelint-config-sass-guidelines": "^5.4.0",
"stylelint-order": "^2.2.1",
"stylelint-scss": "^3.5.4",
"stylelint-selector-bem-pattern": "^2.1.0",
diff --git a/readme.md b/readme.md
index e217bf09f..ec2d77ebe 100644
--- a/readme.md
+++ b/readme.md
@@ -275,7 +275,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
-| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `` or `` element, this will be automatically set to true. |
+| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `` or `` element, this will be automatically set to true. |
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
@@ -305,6 +305,11 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
1. Vimeo only
+2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
+
+- https://webkit.org/blog/6784/new-video-policies-for-ios/
+- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
+- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
# API
@@ -406,6 +411,7 @@ player.fullscreen.active; // false;
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip`¹ | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
+| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
1. HTML5 only
diff --git a/src/js/captions.js b/src/js/captions.js
index ae4642aac..b326d85e1 100644
--- a/src/js/captions.js
+++ b/src/js/captions.js
@@ -124,19 +124,21 @@ 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
+ 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
@@ -300,10 +302,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);
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 +364,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 f276a2df1..6ba6d323b 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -61,7 +61,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
- iconUrl: 'https://cdn.plyr.io/3.5.3/plyr.svg',
+ iconUrl: 'https://cdn.plyr.io/3.5.2/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -195,8 +195,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}', // 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title),fileDetails)&part=snippet',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
@@ -320,9 +319,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
@@ -396,11 +392,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 73903e169..9a960b38b 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -10,20 +10,7 @@ import support from './support';
import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays';
import browser from './utils/browser';
-import {
- createElement,
- emptyElement,
- getAttributesFromSelector,
- getElement,
- getElements,
- hasClass,
- matches,
- removeElement,
- setAttributes,
- setFocus,
- toggleClass,
- toggleHidden,
-} from './utils/elements';
+import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements';
import { off, on } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
@@ -172,7 +159,7 @@ const controls = {
// Create a
createButton(buttonType, attr) {
- const attributes = Object.assign({}, attr);
+ const attributes = extend({}, attr);
let type = toCamelCase(buttonType);
const props = {
@@ -198,8 +185,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 +366,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',
@@ -1138,7 +1127,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 +1183,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 +1236,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 +1245,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));
+ }
- // Buffer progress
- progress.appendChild(controls.createProgress.call(this, 'buffer'));
+ // Fast forward button
+ if (control === 'fast-forward') {
+ container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
+ }
- // TODO: Add loop display indicator
+ // Progress
+ if (control === 'progress') {
+ const progressContainer = createElement('div', {
+ class: `${defaultAttributes.class} plyr__progress__container`,
+ });
- // Seek tooltip
- if (this.config.tooltips.seek) {
- const tooltip = createElement(
- 'span',
- {
- class: this.config.classNames.tooltip,
- },
- '00:00',
- );
+ const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
- progress.appendChild(tooltip);
- this.elements.display.seekTooltip = tooltip;
- }
+ // Seek range slider
+ progress.appendChild(
+ createRange.call(this, 'seek', {
+ id: `plyr-seek-${data.id}`,
+ }),
+ );
- this.elements.progress = progress;
- container.appendChild(this.elements.progress);
- }
+ // Buffer progress
+ progress.appendChild(createProgress.call(this, 'buffer'));
- // Media current time display
- if (this.config.controls.includes('current-time')) {
- container.appendChild(controls.createTime.call(this, 'currentTime'));
- }
+ // TODO: Add loop display indicator
- // 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 control = createElement(
+ 'div',
+ extend({}, defaultAttributes, {
+ class: `${defaultAttributes.class} plyr__menu`.trim(),
hidden: '',
}),
);
- // Bind menu shortcuts for keyboard users
- controls.bindMenuItemShortcuts.call(this, menuItem, type);
+ control.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);
+ control.appendChild(popup);
+ container.appendChild(control);
- // 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 = control;
+ }
- 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/listeners.js b/src/js/listeners.js
index 5a593b10e..d4d7bb32e 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -318,7 +318,7 @@ class Listeners {
const target = player.elements.wrapper.firstChild;
const [, y] = ratio;
- const [videoX, videoY] = getAspectRatio.call(this);
+ const [videoX, videoY] = getAspectRatio.call(player);
target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
target.style.margin = toggle ? '0 auto' : null;
@@ -486,7 +486,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
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js
index c9256b0e0..2b0832852 100644
--- a/src/js/plugins/ads.js
+++ b/src/js/plugins/ads.js
@@ -14,6 +14,20 @@ import loadScript from '../utils/loadScript';
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.
@@ -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()');
@@ -240,9 +261,6 @@ 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));
@@ -297,15 +315,15 @@ class Ads {
triggerEvent.call(this.player, this.player.media, event);
};
+ // 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 +335,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 +372,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 +380,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 +390,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 +475,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();
diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js
index 813bc47e7..3e4b17a3d 100644
--- a/src/js/plugins/previewThumbnails.js
+++ b/src/js/plugins/previewThumbnails.js
@@ -149,9 +149,11 @@ 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 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('/') &&
+ if (
+ !thumbnail.frames[0].text.startsWith('/') &&
!thumbnail.frames[0].text.startsWith('http://') &&
- !thumbnail.frames[0].text.startsWith('https://')) {
+ !thumbnail.frames[0].text.startsWith('https://')
+ ) {
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
}
@@ -297,7 +299,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', {
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index 9d6c16654..8d920eea6 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -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);
@@ -259,7 +259,7 @@ const vimeo = {
.getVideoUrl()
.then(value => {
currentSrc = value;
- controls.setDownloadLink.call(player);
+ controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
@@ -281,7 +281,7 @@ 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}`;
+ player.embed.ratio = [width, height];
setAspectRatio.call(this);
});
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index d862e4dd0..7abc05fec 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -52,9 +52,6 @@ const youtube = {
// 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);
@@ -84,33 +81,27 @@ const youtube = {
// 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
diff --git a/src/js/plyr.js b/src/js/plyr.js
index c252d0526..e81e073e7 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -25,6 +25,7 @@ import { createElement, hasClass, removeElement, replaceElement, toggleClass, wr
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
import is from './utils/is';
import loadSprite from './utils/loadSprite';
+import { clamp } from './utils/numbers';
import { cloneDeep, extend } from './utils/objects';
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
import { parseUrl } from './utils/urls';
@@ -661,24 +662,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,6 +682,42 @@ class Plyr {
return Number(this.media.playbackRate);
}
+ /**
+ * 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
@@ -823,6 +853,19 @@ class Plyr {
return is.url(download) ? download : this.source;
}
+ /**
+ * 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 {String} input - the URL for the new poster image
@@ -851,6 +894,10 @@ 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;
diff --git a/src/js/ui.js b/src/js/ui.js
index 8e50bb839..50de7df10 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -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);
@@ -233,13 +233,16 @@ 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
@@ -248,10 +251,12 @@ const ui = {
if (controls && 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 || controls.pressed || controls.hover || recentTouchSeek),
+ );
}
},
};
diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js
index 6be634e5e..9c1ddebc3 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) {
@@ -137,7 +138,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
const attributes = {};
- const existing = existingAttributes;
+ const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => {
// Remove whitespace
@@ -147,7 +148,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
// 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
@@ -156,11 +157,11 @@ export function getAttributesFromSelector(sel, existingAttributes) {
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 +180,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
}
});
- return attributes;
+ return extend(existing, attributes);
}
// Toggle hidden
diff --git a/src/js/utils/numbers.js b/src/js/utils/numbers.js
new file mode 100644
index 000000000..f6eb65c88
--- /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 191e64615..e51892e58 100644
--- a/src/js/utils/style.js
+++ b/src/js/utils/style.js
@@ -44,8 +44,14 @@ export function getAspectRatio(input) {
}
// Get from embed
- if (ratio === null && !is.empty(this.embed) && is.string(this.embed.ratio)) {
- ratio = parse(this.embed.ratio);
+ if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
+ ({ ratio } = this.embed);
+ }
+
+ // Get from HTML5 video
+ if (ratio === null && this.isHTML5) {
+ const { videoWidth, videoHeight } = this.media;
+ ratio = reduceAspectRatio([videoWidth, videoHeight]);
}
return ratio;
diff --git a/src/sass/components/controls.scss b/src/sass/components/controls.scss
index 41426e8ba..874949573 100644
--- a/src/sass/components/controls.scss
+++ b/src/sass/components/controls.scss
@@ -14,42 +14,46 @@
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;
}
- .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 +66,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/progress.scss b/src/sass/components/progress.scss
index f28a19ca5..04c83516e 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'] {