aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSam Potts <sam@potts.es>2019-06-01 19:55:14 +1000
committerSam Potts <sam@potts.es>2019-06-01 19:55:14 +1000
commit12c6282d14e3e06a7784760f83affc9195afed1f (patch)
treeaf1c6be692c4cf2679e7efc604896b686974368d /src
parent996075decc6e8c0f0c5059dccea21b16020eb78b (diff)
parent0249772f019762ebd494ac409e597103820413c3 (diff)
downloadplyr-12c6282d14e3e06a7784760f83affc9195afed1f.tar.lz
plyr-12c6282d14e3e06a7784760f83affc9195afed1f.tar.xz
plyr-12c6282d14e3e06a7784760f83affc9195afed1f.zip
Merge branch 'develop' into css-variables
# Conflicts: # .eslintrc # demo/dist/demo.css # demo/dist/demo.js # demo/dist/demo.min.js # demo/dist/demo.min.js.map # dist/plyr.css # dist/plyr.js # dist/plyr.min.js # dist/plyr.min.js.map # dist/plyr.min.mjs # dist/plyr.min.mjs.map # dist/plyr.mjs # dist/plyr.polyfilled.js # dist/plyr.polyfilled.min.js # dist/plyr.polyfilled.min.js.map # dist/plyr.polyfilled.min.mjs # dist/plyr.polyfilled.min.mjs.map # dist/plyr.polyfilled.mjs # gulpfile.js # package.json
Diffstat (limited to 'src')
-rw-r--r--src/js/captions.js33
-rw-r--r--src/js/config/defaults.js17
-rw-r--r--src/js/controls.js574
-rw-r--r--src/js/html5.js9
-rw-r--r--src/js/listeners.js33
-rw-r--r--src/js/plugins/ads.js91
-rw-r--r--src/js/plugins/previewThumbnails.js28
-rw-r--r--src/js/plugins/vimeo.js12
-rw-r--r--src/js/plugins/youtube.js102
-rw-r--r--src/js/plyr.js132
-rw-r--r--src/js/plyr.polyfilled.js3
-rw-r--r--src/js/ui.js42
-rw-r--r--src/js/utils/elements.js22
-rw-r--r--src/js/utils/events.js1
-rw-r--r--src/js/utils/i18n.js4
-rw-r--r--src/js/utils/loadSprite.js3
-rw-r--r--src/js/utils/numbers.js17
-rw-r--r--src/js/utils/style.js70
-rw-r--r--src/js/utils/time.js1
-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/plyr.scss1
24 files changed, 765 insertions, 578 deletions
diff --git a/src/js/captions.js b/src/js/captions.js
index ae4642aa..f7c24534 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,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
+ Object.assign(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 +165,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 +300,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 +362,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 82809511..6ba6d323 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,
@@ -194,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',
@@ -319,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
@@ -330,6 +327,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 +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 73903e16..43a92140 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';
@@ -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/html5.js b/src/js/html5.js
index 3266a58e..e538e922 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,15 @@ const html5 = {
const player = this;
+ // Set aspect ratio if set
+ 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 +60,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..f5c9cda8 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) {
@@ -317,10 +316,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,8 +337,8 @@ 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 => {
@@ -351,7 +350,6 @@ class Listeners {
}
const isEnter = event.type === 'enterfullscreen';
-
// Set the player size when entering fullscreen to viewport size
const { padding, ratio } = setPlayerSize(isEnter);
@@ -486,7 +484,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
@@ -542,7 +540,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 +675,6 @@ class Listeners {
// Was playing before?
const play = seek.hasAttribute(attribute);
-
// Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
@@ -706,7 +702,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 +801,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 +832,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 c9256b0e..e6fab967 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,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();
diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js
index bd7a6bbd..b4714117 100644
--- a/src/js/plugins/previewThumbnails.js
+++ b/src/js/plugins/previewThumbnails.js
@@ -2,6 +2,7 @@ import { createElement } from '../utils/elements';
import { once } from '../utils/events';
import fetch from '../utils/fetch';
import is from '../utils/is';
+import { extend } from '../utils/objects';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
@@ -17,17 +18,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]}`);
@@ -121,7 +122,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 +148,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);
}
@@ -294,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', {
@@ -419,7 +426,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;
@@ -460,7 +469,6 @@ class PreviewThumbnails {
const { urlPrefix } = this.thumbnails[0];
const thumbURL = urlPrefix + newThumbFilename;
-
const previewImage = new Image();
previewImage.src = thumbURL;
previewImage.onload = () => {
@@ -594,11 +602,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;
@@ -629,9 +635,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..bef48708 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);
@@ -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 d5972c80..34c5de7e 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -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://i.ytimg.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 0d3d1674..83897cab 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -1,6 +1,6 @@
// ==========================================================================
// Plyr
-// plyr.js v3.5.2
+// plyr.js v3.5.3
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
@@ -25,7 +25,9 @@ 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';
// 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;
}
/**
@@ -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;
@@ -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,6 +681,42 @@ 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
@@ -823,6 +852,19 @@ 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 {String} input - the URL for the new poster image
*/
@@ -847,6 +889,38 @@ 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
*/
@@ -969,10 +1043,8 @@ class Plyr {
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);
@@ -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);
diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js
index 8623e41a..0f1e7e25 100644
--- a/src/js/plyr.polyfilled.js
+++ b/src/js/plyr.polyfilled.js
@@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
-// plyr.js v3.5.2
+// plyr.js v3.5.3
// 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..df52eb64 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);
@@ -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/elements.js b/src/js/utils/elements.js
index 6be634e5..98b44f13 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
@@ -233,14 +231,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/loadSprite.js b/src/js/utils/loadSprite.js
index 917bd6ac..fe4add00 100644
--- a/src/js/utils/loadSprite.js
+++ b/src/js/utils/loadSprite.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..6f3069c9 100644
--- a/src/js/utils/style.js
+++ b/src/js/utils/style.js
@@ -4,26 +4,68 @@
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];
+}
+
+export function getAspectRatio(input) {
+ const parse = ratio => {
+ if (!validateRatio(ratio)) {
+ return null;
+ }
- if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) {
+ return ratio.split(':').map(Number);
+ };
+
+ // 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 +74,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/sass/components/controls.scss b/src/sass/components/controls.scss
index 41426e8b..f4559bba 100644
--- a/src/sass/components/controls.scss
+++ b/src/sass/components/controls.scss
@@ -12,44 +12,49 @@
align-items: center;
display: flex;
justify-content: flex-end;
+ min-width: 0; // Fix for Edge issue where content would overflow
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 +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/plyr.scss b/src/sass/plyr.scss
index 7d69871c..1b40f7a9 100644
--- a/src/sass/plyr.scss
+++ b/src/sass/plyr.scss
@@ -32,7 +32,6 @@ $css-vars-use-native: true;
@import 'components/captions';
@import 'components/control';
@import 'components/controls';
-@import 'components/embed';
@import 'components/menus';
@import 'components/sliders';
@import 'components/poster';