aboutsummaryrefslogtreecommitdiffstats
path: root/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js')
-rw-r--r--src/js/captions.js26
-rw-r--r--src/js/config/defaults.js9
-rw-r--r--src/js/controls.js52
-rw-r--r--src/js/fullscreen.js16
-rw-r--r--src/js/html5.js8
-rw-r--r--src/js/listeners.js99
-rw-r--r--src/js/media.js1
-rw-r--r--src/js/plugins/ads.js20
-rw-r--r--src/js/plugins/preview-thumbnails.js24
-rw-r--r--src/js/plugins/vimeo.js64
-rw-r--r--src/js/plugins/youtube.js71
-rw-r--r--src/js/plyr.d.ts109
-rw-r--r--src/js/plyr.js17
-rw-r--r--src/js/source.js2
-rw-r--r--src/js/ui.js13
-rw-r--r--src/js/utils/animation.js2
-rw-r--r--src/js/utils/elements.js4
-rw-r--r--src/js/utils/events.js6
-rw-r--r--src/js/utils/is.js38
-rw-r--r--src/js/utils/load-sprite.js2
-rw-r--r--src/js/utils/objects.js2
-rw-r--r--src/js/utils/strings.js2
-rw-r--r--src/js/utils/style.js8
-rw-r--r--src/js/utils/time.js8
24 files changed, 340 insertions, 263 deletions
diff --git a/src/js/captions.js b/src/js/captions.js
index ebb678f8..98d7d613 100644
--- a/src/js/captions.js
+++ b/src/js/captions.js
@@ -56,7 +56,7 @@ const captions = {
if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track');
- Array.from(elements).forEach(track => {
+ Array.from(elements).forEach((track) => {
const src = track.getAttribute('src');
const url = parseUrl(src);
@@ -66,7 +66,7 @@ const captions = {
['http:', 'https:'].includes(url.protocol)
) {
fetch(src, 'blob')
- .then(blob => {
+ .then((blob) => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
@@ -84,7 +84,7 @@ const captions = {
// * toggled: The real captions state
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
- const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
+ 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'
@@ -119,13 +119,13 @@ const captions = {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
- const languageExists = Boolean(tracks.find(track => track.language === language));
+ const languageExists = Boolean(tracks.find((track) => track.language === language));
// Handle tracks (add event listener and "pseudo"-default)
if (this.isHTML5 && this.isVideo) {
tracks
- .filter(track => !meta.get(track))
- .forEach(track => {
+ .filter((track) => !meta.get(track))
+ .forEach((track) => {
this.debug.log('Track added', track);
// Attempt to store if the original dom element was "default"
@@ -309,19 +309,19 @@ const captions = {
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
- .filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
- .filter(track => ['captions', 'subtitles'].includes(track.kind));
+ .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))
+ .filter((track) => ['captions', 'subtitles'].includes(track.kind));
},
// Match tracks based on languages and get the first
findTrack(languages, force = false) {
const tracks = captions.getTracks.call(this);
- const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
+ 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(t => t.language === language);
+ languages.every((language) => {
+ track = sorted.find((t) => t.language === language);
return !track; // Break iteration if there is a match
});
@@ -383,12 +383,12 @@ const captions = {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
- .map(cue => cue.getCueAsHTML())
+ .map((cue) => cue.getCueAsHTML())
.map(getHTML);
}
// Set new caption text
- const content = cues.map(cueText => cueText.trim()).join('\n');
+ const content = cues.map((cueText) => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js
index 03c75150..a199d316 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -422,20 +422,23 @@ const defaults = {
title: false,
speed: true,
transparent: false,
+ // Custom settings from Plyr
+ customControls: true,
+ referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
- // Custom settings from Plyr
- referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin
youtube: {
- noCookie: true, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
+ // Custom settings from Plyr
+ customControls: true,
+ noCookie: false, // Whether to use an alternative version of YouTube without cookies
},
};
diff --git a/src/js/controls.js b/src/js/controls.js
index ad126de1..ff20982e 100644
--- a/src/js/controls.js
+++ b/src/js/controls.js
@@ -179,7 +179,7 @@ const controls = {
iconPressed: null,
};
- ['element', 'icon', 'label'].forEach(key => {
+ ['element', 'icon', 'label'].forEach((key) => {
if (Object.keys(attributes).includes(key)) {
props[key] = attributes[key];
delete attributes[key];
@@ -193,7 +193,7 @@ const controls = {
// Set class name
if (Object.keys(attributes).includes('class')) {
- if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
+ if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {
extend(attributes, {
class: `${attributes.class} ${this.config.classNames.control}`,
});
@@ -401,7 +401,7 @@ const controls = {
this,
menuItem,
'keydown keyup',
- event => {
+ (event) => {
// We only care about space and ⬆️ ⬇️️ ➡️
if (![32, 38, 39, 40].includes(event.which)) {
return;
@@ -448,7 +448,7 @@ const controls = {
// Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here
- on.call(this, menuItem, 'keyup', event => {
+ on.call(this, menuItem, 'keyup', (event) => {
if (event.which !== 13) {
return;
}
@@ -493,8 +493,8 @@ const controls = {
// Ensure exclusivity
if (check) {
Array.from(menuItem.parentNode.children)
- .filter(node => matches(node, '[role="menuitemradio"]'))
- .forEach(node => node.setAttribute('aria-checked', 'false'));
+ .filter((node) => matches(node, '[role="menuitemradio"]'))
+ .forEach((node) => node.setAttribute('aria-checked', 'false'));
}
menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
@@ -504,7 +504,7 @@ const controls = {
this.listeners.bind(
menuItem,
'click keyup',
- event => {
+ (event) => {
if (is.keyboardEvent(event) && event.which !== 32) {
return;
}
@@ -698,7 +698,7 @@ const controls = {
}
const visible = `${this.config.classNames.tooltip}--visible`;
- const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
+ const toggle = (show) => toggleClass(this.elements.display.seekTooltip, visible, show);
// Hide on touch
if (this.touch) {
@@ -894,7 +894,7 @@ const controls = {
// Set options if passed and filter based on uniqueness and config
if (is.array(options)) {
- this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
+ this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));
}
// Toggle the pane and tab
@@ -913,7 +913,7 @@ const controls = {
}
// Get the badge HTML for HD, 4K etc
- const getBadge = quality => {
+ const getBadge = (quality) => {
const label = i18n.get(`qualityBadge.${quality}`, this.config);
if (!label.length) {
@@ -929,7 +929,7 @@ const controls = {
const sorting = this.config.quality.options;
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
})
- .forEach(quality => {
+ .forEach((quality) => {
controls.createMenuItem.call(this, {
value: quality,
list,
@@ -1052,7 +1052,7 @@ const controls = {
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
// Filter out invalid speeds
- this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);
+ this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);
// Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
@@ -1070,7 +1070,7 @@ const controls = {
}
// Create items
- this.options.speed.forEach(speed => {
+ this.options.speed.forEach((speed) => {
controls.createMenuItem.call(this, {
value: speed,
list,
@@ -1085,7 +1085,7 @@ const controls = {
// Check if we need to hide/show the settings menu
checkMenu() {
const { buttons } = this.elements.settings;
- const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);
+ const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);
toggleHidden(this.elements.settings.menu, !visible);
},
@@ -1099,7 +1099,7 @@ const controls = {
let target = pane;
if (!is.element(target)) {
- target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
+ target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);
}
const firstItem = target.querySelector('[role^="menuitem"]');
@@ -1191,7 +1191,7 @@ const controls = {
// Hide all other panels
const container = target.parentNode;
- const current = Array.from(container.children).find(node => !node.hidden);
+ const current = Array.from(container.children).find((node) => !node.hidden);
// If we can do fancy animations, we'll animate the height/width
if (support.transitions && !support.reducedMotion) {
@@ -1203,7 +1203,7 @@ const controls = {
const size = controls.getMenuSize.call(this, target);
// Restore auto height/width
- const restore = event => {
+ const restore = (event) => {
// We're only bothered about height and width on the container
if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
return;
@@ -1275,7 +1275,7 @@ const controls = {
const defaultAttributes = { class: 'plyr__controls__item' };
// Loop through controls in order
- dedupe(is.array(this.config.controls) ? this.config.controls: []).forEach(control => {
+ dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {
// Restart button
if (control === 'restart') {
container.appendChild(createButton.call(this, 'restart', defaultAttributes));
@@ -1437,7 +1437,7 @@ const controls = {
this.elements.settings.panels.home = home;
// Build the menu items
- this.config.settings.forEach(type => {
+ this.config.settings.forEach((type) => {
// TODO: bundle this with the createMenuItem helper and bindings
const menuItem = createElement(
'button',
@@ -1510,7 +1510,7 @@ const controls = {
this,
pane,
'keydown',
- event => {
+ (event) => {
// We only care about <-
if (event.which !== 37) {
return;
@@ -1661,7 +1661,7 @@ const controls = {
}
// Replace props with their value
- const replace = input => {
+ const replace = (input) => {
let result = input;
Object.entries(props).forEach(([key, value]) => {
@@ -1702,7 +1702,7 @@ const controls = {
// Add pressed property to buttons
if (!is.empty(this.elements.buttons)) {
- const addProperty = button => {
+ const addProperty = (button) => {
const className = this.config.classNames.controlPressed;
Object.defineProperty(button, 'pressed', {
enumerable: true,
@@ -1718,11 +1718,9 @@ const controls = {
// Toggle classname when pressed property is set
Object.values(this.elements.buttons)
.filter(Boolean)
- .forEach(button => {
+ .forEach((button) => {
if (is.array(button) || is.nodeList(button)) {
- Array.from(button)
- .filter(Boolean)
- .forEach(addProperty);
+ Array.from(button).filter(Boolean).forEach(addProperty);
} else {
addProperty(button);
}
@@ -1740,7 +1738,7 @@ const controls = {
const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;
const labels = getElements.call(this, selector);
- Array.from(labels).forEach(label => {
+ Array.from(labels).forEach((label) => {
toggleClass(label, this.config.classNames.hidden, false);
toggleClass(label, this.config.classNames.tooltip, true);
});
diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js
index 5029e7de..7bb22391 100644
--- a/src/js/fullscreen.js
+++ b/src/js/fullscreen.js
@@ -5,7 +5,7 @@
// ==========================================================================
import browser from './utils/browser';
-import { closest,getElements, hasClass, toggleClass } from './utils/elements';
+import { closest, getElements, hasClass, toggleClass } from './utils/elements';
import { on, triggerEvent } from './utils/events';
import is from './utils/is';
import { silencePromise } from './utils/promise';
@@ -43,17 +43,17 @@ class Fullscreen {
);
// Fullscreen toggle on double click
- on.call(this.player, this.player.elements.container, 'dblclick', event => {
+ on.call(this.player, this.player.elements.container, 'dblclick', (event) => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
- this.toggle();
+ this.player.listeners.proxy(event, this.toggle, 'fullscreen');
});
// Tap focus when in fullscreen
- on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
+ on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));
// Update the UI
this.update();
@@ -85,7 +85,7 @@ class Fullscreen {
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
- prefixes.some(pre => {
+ prefixes.some((pre) => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
@@ -145,8 +145,10 @@ class Fullscreen {
button.pressed = this.active;
}
+ // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
+ const target = this.target === this.player.media ? this.target : this.player.elements.container;
// Trigger an event
- triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
+ triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
@@ -189,7 +191,7 @@ class Fullscreen {
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
- .filter(part => part.trim() !== property)
+ .filter((part) => part.trim() !== property)
.join(',');
}
}
diff --git a/src/js/html5.js b/src/js/html5.js
index 658abf15..0cb5b27a 100644
--- a/src/js/html5.js
+++ b/src/js/html5.js
@@ -18,7 +18,7 @@ const html5 = {
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
- return sources.filter(source => {
+ return sources.filter((source) => {
const type = source.getAttribute('type');
if (is.empty(type)) {
@@ -39,7 +39,7 @@ const html5 = {
// Get sizes from <source> elements
return html5.getSources
.call(this)
- .map(source => Number(source.getAttribute('size')))
+ .map((source) => Number(source.getAttribute('size')))
.filter(Boolean);
},
@@ -63,7 +63,7 @@ const html5 = {
get() {
// Get sources
const sources = html5.getSources.call(player);
- const source = sources.find(s => s.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'));
@@ -80,7 +80,7 @@ const html5 = {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
- const source = sources.find(s => Number(s.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 2cc71537..48734bcf 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -277,7 +277,7 @@ class Listeners {
player,
elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
- event => {
+ (event) => {
const { controls: controlsElement } = elements;
// Remove button states for fullscreen
@@ -319,7 +319,7 @@ class Listeners {
};
// Resize on fullscreen change
- const setPlayerSize = measure => {
+ const setPlayerSize = (measure) => {
// If we don't need to measure the viewport
if (!measure) {
return setAspectRatio.call(player);
@@ -336,7 +336,7 @@ class Listeners {
timers.resized = setTimeout(setPlayerSize, 50);
};
- on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
+ on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {
const { target, usingNative } = player.fullscreen;
// Ignore events not from target
@@ -356,6 +356,11 @@ class Listeners {
// Set Vimeo gutter
setGutter(ratio, padding, isEnter);
+ // Horrible hack for Safari 14 not repainting properly on entering fullscreen
+ if (isEnter) {
+ setTimeout(() => repaint(elements.container), 100);
+ }
+
// If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) {
if (isEnter) {
@@ -373,10 +378,10 @@ class Listeners {
const { elements } = player;
// Time change on media
- on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));
+ on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));
// Display duration
- on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event =>
+ on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>
controls.durationUpdate.call(player, event),
);
@@ -393,20 +398,20 @@ class Listeners {
});
// Check for buffer progress
- on.call(player, player.media, 'progress playing seeking seeked', event =>
+ on.call(player, player.media, 'progress playing seeking seeked', (event) =>
controls.updateProgress.call(player, event),
);
// Handle volume changes
- on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));
+ on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));
// Handle play/pause
- on.call(player, player.media, 'playing play pause ended emptied timeupdate', event =>
+ on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>
ui.checkPlaying.call(player, event),
);
// Loading state
- on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));
+ on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));
// Click video
if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
@@ -419,7 +424,7 @@ class Listeners {
}
// On click play, pause or restart
- on.call(player, elements.container, 'click', event => {
+ on.call(player, elements.container, 'click', (event) => {
const targets = [elements.container, wrapper];
// Ignore if click if not container or in video wrapper
@@ -459,7 +464,7 @@ class Listeners {
player,
elements.wrapper,
'contextmenu',
- event => {
+ (event) => {
event.preventDefault();
},
false,
@@ -485,7 +490,7 @@ class Listeners {
});
// Quality change
- on.call(player, player.media, 'qualitychange', event => {
+ on.call(player, player.media, 'qualitychange', (event) => {
// Update UI
controls.updateSetting.call(player, 'quality', null, event.detail.quality);
});
@@ -499,7 +504,7 @@ class Listeners {
// Bubble up key events for Edge
const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
- on.call(player, player.media, proxyEvents, event => {
+ on.call(player, player.media, proxyEvents, (event) => {
let { detail = {} } = event;
// Get error details from media
@@ -539,7 +544,7 @@ class Listeners {
player,
element,
type,
- event => this.proxy(event, defaultHandler, customHandlerKey),
+ (event) => this.proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler,
);
}
@@ -553,7 +558,7 @@ class Listeners {
// Play/pause toggle
if (elements.buttons.play) {
- Array.from(elements.buttons.play).forEach(button => {
+ Array.from(elements.buttons.play).forEach((button) => {
this.bind(
button,
'click',
@@ -569,10 +574,28 @@ class Listeners {
this.bind(elements.buttons.restart, 'click', player.restart, 'restart');
// Rewind
- this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind');
+ this.bind(
+ elements.buttons.rewind,
+ 'click',
+ () => {
+ // Record seek time so we can prevent hiding controls for a few seconds after rewind
+ player.lastSeekTime = Date.now();
+ player.rewind();
+ },
+ 'rewind',
+ );
// Rewind
- this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward');
+ this.bind(
+ elements.buttons.fastForward,
+ 'click',
+ () => {
+ // Record seek time so we can prevent hiding controls for a few seconds after fast forward
+ player.lastSeekTime = Date.now();
+ player.forward();
+ },
+ 'fastForward',
+ );
// Mute toggle
this.bind(
@@ -624,7 +647,7 @@ class Listeners {
this.bind(
elements.buttons.settings,
'click',
- event => {
+ (event) => {
// Prevent the document click listener closing the menu
event.stopPropagation();
event.preventDefault();
@@ -641,7 +664,7 @@ class Listeners {
this.bind(
elements.buttons.settings,
'keyup',
- event => {
+ (event) => {
const code = event.which;
// We only care about space and return
@@ -669,21 +692,21 @@ class Listeners {
);
// Escape closes menu
- this.bind(elements.settings.menu, 'keydown', event => {
+ this.bind(elements.settings.menu, 'keydown', (event) => {
if (event.which === 27) {
controls.toggleMenu.call(player, event);
}
});
// Set range input alternative "value", which matches the tooltip time (#954)
- this.bind(elements.inputs.seek, 'mousedown mousemove', event => {
+ this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {
const rect = elements.progress.getBoundingClientRect();
const percent = (100 / rect.width) * (event.pageX - rect.left);
event.currentTarget.setAttribute('seek-value', percent);
});
// Pause while seeking
- this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
+ this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {
const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which;
const attribute = 'play-on-seeked';
@@ -715,14 +738,14 @@ class Listeners {
// it takes over further interactions on the page. This is a hack
if (browser.isIos) {
const inputs = getElements.call(player, 'input[type="range"]');
- Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));
+ Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));
}
// Seek
this.bind(
elements.inputs.seek,
inputEvent,
- event => {
+ (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');
@@ -739,13 +762,13 @@ class Listeners {
);
// Seek tooltip
- this.bind(elements.progress, 'mouseenter mouseleave mousemove', event =>
+ this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>
controls.updateSeekTooltip.call(player, event),
);
// Preview thumbnails plugin
// TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this
- this.bind(elements.progress, 'mousemove touchmove', event => {
+ this.bind(elements.progress, 'mousemove touchmove', (event) => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {
@@ -763,7 +786,7 @@ class Listeners {
});
// Show scrubbing preview
- this.bind(elements.progress, 'mousedown touchstart', event => {
+ this.bind(elements.progress, 'mousedown touchstart', (event) => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {
@@ -771,7 +794,7 @@ class Listeners {
}
});
- this.bind(elements.progress, 'mouseup touchend', event => {
+ this.bind(elements.progress, 'mouseup touchend', (event) => {
const { previewThumbnails } = player;
if (previewThumbnails && previewThumbnails.loaded) {
@@ -781,8 +804,8 @@ class Listeners {
// Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) {
- Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => {
- this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));
+ Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => {
+ this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
});
}
@@ -805,30 +828,30 @@ class Listeners {
this.bind(
elements.inputs.volume,
inputEvent,
- event => {
+ (event) => {
player.volume = event.target.value;
},
'volume',
);
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
- this.bind(elements.controls, 'mouseenter mouseleave', event => {
+ this.bind(elements.controls, 'mouseenter mouseleave', (event) => {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
// Also update controls.hover state for any non-player children of fullscreen element (as above)
if (elements.fullscreen) {
Array.from(elements.fullscreen.children)
- .filter(c => !c.contains(elements.container))
- .forEach(child => {
- this.bind(child, 'mouseenter mouseleave', event => {
+ .filter((c) => !c.contains(elements.container))
+ .forEach((child) => {
+ this.bind(child, 'mouseenter mouseleave', (event) => {
elements.controls.hover = !player.touch && event.type === 'mouseenter';
});
});
}
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
- this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
+ this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
});
@@ -861,12 +884,12 @@ class Listeners {
this.bind(
elements.inputs.volume,
'wheel',
- event => {
+ (event) => {
// 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));
+ 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/media.js b/src/js/media.js
index 4584fea3..ddac5ebf 100644
--- a/src/js/media.js
+++ b/src/js/media.js
@@ -41,6 +41,7 @@ const media = {
// Poster image container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
+ hidden: '',
});
this.elements.wrapper.appendChild(this.elements.poster);
diff --git a/src/js/plugins/ads.js b/src/js/plugins/ads.js
index 1a52ebce..12b5cc31 100644
--- a/src/js/plugins/ads.js
+++ b/src/js/plugins/ads.js
@@ -15,7 +15,7 @@ import { silencePromise } from '../utils/promise';
import { formatTime } from '../utils/time';
import { buildUrlParams } from '../utils/urls';
-const destroy = instance => {
+const destroy = (instance) => {
// Destroy our adsManager
if (instance.manager) {
instance.manager.destroy();
@@ -179,10 +179,10 @@ class Ads {
// Listen and respond to ads loaded and error events
this.loader.addEventListener(
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
- event => this.onAdsManagerLoaded(event),
+ (event) => this.onAdsManagerLoaded(event),
false,
);
- this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
+ this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);
// Request video ads to be pre-loaded
this.requestAds();
@@ -264,11 +264,11 @@ class Ads {
// Add listeners to the required events
// Advertisement error events
- this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
+ 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], e => this.onAdEvent(e));
+ Object.keys(google.ima.AdEvent.Type).forEach((type) => {
+ this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));
});
// Resolve our adsManager
@@ -278,7 +278,7 @@ class Ads {
addCuePoints() {
// Add advertisement cue's within the time line if available
if (!is.empty(this.cuePoints)) {
- this.cuePoints.forEach(cuePoint => {
+ this.cuePoints.forEach((cuePoint) => {
if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
@@ -310,7 +310,7 @@ class Ads {
const adData = event.getAdData();
// Proxy event
- const dispatchEvent = type => {
+ const dispatchEvent = (type) => {
triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
};
@@ -565,7 +565,7 @@ class Ads {
}
// Re-set our adsManager promises
- this.managerPromise = new Promise(resolve => {
+ this.managerPromise = new Promise((resolve) => {
this.on('loaded', resolve);
this.player.debug.log(this.manager);
});
@@ -586,7 +586,7 @@ class Ads {
const handlers = this.events[event];
if (is.array(handlers)) {
- handlers.forEach(handler => {
+ handlers.forEach((handler) => {
if (is.function(handler)) {
handler.apply(this, args);
}
diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js
index 6ce53f28..16167247 100644
--- a/src/js/plugins/preview-thumbnails.js
+++ b/src/js/plugins/preview-thumbnails.js
@@ -5,15 +5,15 @@ import is from '../utils/is';
import { formatTime } from '../utils/time';
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
-const parseVtt = vttDataString => {
+const parseVtt = (vttDataString) => {
const processedList = [];
const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
- frames.forEach(frame => {
+ frames.forEach((frame) => {
const result = {};
const lines = frame.split(/\r\n|\n|\r/);
- lines.forEach(line => {
+ lines.forEach((line) => {
if (!is.number(result.startTime)) {
// The line with start and end times on it is the first line of interest
const matchTimes = line.match(
@@ -130,7 +130,7 @@ class PreviewThumbnails {
// Download VTT files and parse them
getThumbnails() {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
const { src } = this.player.config.previewThumbnails;
if (is.empty(src)) {
@@ -149,7 +149,7 @@ class PreviewThumbnails {
// Via callback()
if (is.function(src)) {
- src(thumbnails => {
+ src((thumbnails) => {
this.thumbnails = thumbnails;
sortAndResolve();
});
@@ -159,7 +159,7 @@ 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));
+ const promises = urls.map((u) => this.getThumbnail(u));
// Resolve
Promise.all(promises).then(sortAndResolve);
}
@@ -168,8 +168,8 @@ class PreviewThumbnails {
// Process individual VTT file
getThumbnail(url) {
- return new Promise(resolve => {
- fetch(url).then(response => {
+ return new Promise((resolve) => {
+ fetch(url).then((response) => {
const thumbnail = {
frames: parseVtt(response),
height: null,
@@ -360,7 +360,7 @@ class PreviewThumbnails {
// Find the desired thumbnail index
// TODO: Handle a video longer than the thumbs where thumbNum is null
const thumbNum = this.thumbnails[0].frames.findIndex(
- frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
+ (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,
);
const hasThumb = thumbNum >= 0;
let qualityIndex = 0;
@@ -454,7 +454,7 @@ class PreviewThumbnails {
// Remove all preview images that aren't the designated current image
removeOldImages(currentImage) {
// Get a list of all images, convert it from a DOM list to an array
- Array.from(this.currentImageContainer.children).forEach(image => {
+ Array.from(this.currentImageContainer.children).forEach((image) => {
if (image.tagName.toLowerCase() !== 'img') {
return;
}
@@ -481,7 +481,7 @@ class PreviewThumbnails {
// Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
// This will only preload the lowest quality
preloadNearby(thumbNum, forward = true) {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
setTimeout(() => {
const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
@@ -496,7 +496,7 @@ class PreviewThumbnails {
let foundOne = false;
- thumbnailsClone.forEach(frame => {
+ thumbnailsClone.forEach((frame) => {
const newThumbFilename = frame.text;
if (newThumbFilename !== oldThumbFilename) {
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index 33c327d7..b050cc53 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -58,7 +58,7 @@ const vimeo = {
.then(() => {
vimeo.ready.call(player);
})
- .catch(error => {
+ .catch((error) => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
@@ -112,25 +112,29 @@ const vimeo = {
}
// Inject the package
- const { poster } = player;
- if (premium) {
- iframe.setAttribute('data-poster', poster);
+ if (premium || !config.customControls) {
+ iframe.setAttribute('data-poster', player.poster);
player.media = replaceElement(iframe, player.media);
} else {
- const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster });
+ const wrapper = createElement('div', {
+ class: player.config.classNames.embedContainer,
+ 'data-poster': player.poster,
+ });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
-
+
// Get poster image
- fetch(format(player.config.urls.vimeo.api, src)).then(response => {
- if (is.empty(response) || !response.thumbnail_url) {
- return;
- }
-
- // Set and show poster
- ui.setPoster.call(player, response.thumbnail_url).catch(() => { });
- });
+ if (!config.customControls) {
+ fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
+ if (is.empty(response) || !response.thumbnail_url) {
+ return;
+ }
+
+ // Set and show poster
+ ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
+ });
+ }
// Setup instance
// https://github.com/vimeo/player.js
@@ -263,11 +267,11 @@ const vimeo = {
let currentSrc;
player.embed
.getVideoUrl()
- .then(value => {
+ .then((value) => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
- .catch(error => {
+ .catch((error) => {
this.debug.warn(error);
});
@@ -285,49 +289,49 @@ const vimeo = {
});
// Set aspect ratio based on video size
- Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
+ Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {
const [width, height] = dimensions;
player.embed.ratio = [width, height];
setAspectRatio.call(this);
});
// Set autopause
- player.embed.setAutopause(player.config.autopause).then(state => {
+ player.embed.setAutopause(player.config.autopause).then((state) => {
player.config.autopause = state;
});
// Get title
- player.embed.getVideoTitle().then(title => {
+ player.embed.getVideoTitle().then((title) => {
player.config.title = title;
ui.setTitle.call(this);
});
// Get current time
- player.embed.getCurrentTime().then(value => {
+ player.embed.getCurrentTime().then((value) => {
currentTime = value;
triggerEvent.call(player, player.media, 'timeupdate');
});
// Get duration
- player.embed.getDuration().then(value => {
+ player.embed.getDuration().then((value) => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Get captions
- player.embed.getTextTracks().then(tracks => {
+ player.embed.getTextTracks().then((tracks) => {
player.media.textTracks = tracks;
captions.setup.call(player);
});
player.embed.on('cuechange', ({ cues = [] }) => {
- const strippedCues = cues.map(cue => stripHTML(cue.text));
+ const strippedCues = cues.map((cue) => stripHTML(cue.text));
captions.updateCues.call(player, strippedCues);
});
player.embed.on('loaded', () => {
// Assure state and events are updated on autoplay
- player.embed.getPaused().then(paused => {
+ player.embed.getPaused().then((paused) => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
@@ -360,13 +364,13 @@ const vimeo = {
assurePlaybackState.call(player, false);
});
- player.embed.on('timeupdate', data => {
+ player.embed.on('timeupdate', (data) => {
player.media.seeking = false;
currentTime = data.seconds;
triggerEvent.call(player, player.media, 'timeupdate');
});
- player.embed.on('progress', data => {
+ player.embed.on('progress', (data) => {
player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'progress');
@@ -377,7 +381,7 @@ const vimeo = {
// Get duration as if we do it before load, it gives an incorrect value
// https://github.com/sampotts/plyr/issues/891
- player.embed.getDuration().then(value => {
+ player.embed.getDuration().then((value) => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
@@ -395,13 +399,15 @@ const vimeo = {
triggerEvent.call(player, player.media, 'ended');
});
- player.embed.on('error', detail => {
+ player.embed.on('error', (detail) => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
- setTimeout(() => ui.build.call(player), 0);
+ if (config.customControls) {
+ setTimeout(() => ui.build.call(player), 0);
+ }
},
};
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 89a75d89..db5781e6 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -70,7 +70,7 @@ const youtube = {
};
// Load the SDK
- loadScript(this.config.urls.youtube.sdk).catch(error => {
+ loadScript(this.config.urls.youtube.sdk).catch((error) => {
this.debug.warn('YouTube API failed to load', error);
});
}
@@ -81,7 +81,7 @@ const youtube = {
const url = format(this.config.urls.youtube.api, videoId);
fetch(url)
- .then(data => {
+ .then((data) => {
if (is.object(data)) {
const { title, height, width } = data;
@@ -104,6 +104,7 @@ const youtube = {
// API ready
ready() {
const player = this;
+ const config = player.config.youtube;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
@@ -121,43 +122,46 @@ 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, 'data-poster': poster });
+ const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
player.media = replaceElement(container, player.media);
- // Id to poster wrapper
- 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(src => {
- // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
- if (!src.includes('maxres')) {
- player.elements.poster.style.backgroundSize = 'cover';
- }
- })
- .catch(() => {});
-
- const config = player.config.youtube;
+ // Only load the poster when using custom controls
+ if (config.customControls) {
+ 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((src) => {
+ // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
+ if (!src.includes('maxres')) {
+ player.elements.poster.style.backgroundSize = 'cover';
+ }
+ })
+ .catch(() => {});
+ }
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
- player.embed = new window.YT.Player(id, {
+ player.embed = new window.YT.Player(player.media, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
- autoplay: player.config.autoplay ? 1 : 0, // Autoplay
- hl: player.config.hl, // iframe interface language
- controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
- disablekb: 1, // Disable keyboard as we handle it
- playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
+ // Autoplay
+ autoplay: player.config.autoplay ? 1 : 0,
+ // iframe interface language
+ hl: player.config.hl,
+ // Only show controls if not fully supported or opted out
+ controls: player.supported.ui && config.customControls ? 0 : 1,
+ // Disable keyboard as we handle it
+ disablekb: 1,
+ // Allow iOS inline playback
+ playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
@@ -278,6 +282,7 @@ const youtube = {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
+ instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -299,10 +304,10 @@ const youtube = {
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
- player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
+ player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
- if (player.supported.ui) {
+ if (player.supported.ui && config.customControls) {
player.media.setAttribute('tabindex', -1);
}
@@ -335,7 +340,9 @@ const youtube = {
}, 200);
// Rebuild UI
- setTimeout(() => ui.build.call(player), 50);
+ if (config.customControls) {
+ setTimeout(() => ui.build.call(player), 50);
+ }
},
onStateChange(event) {
// Get the instance
@@ -386,7 +393,7 @@ const youtube = {
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
- if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
+ if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts
index 13523b35..4b332aeb 100644
--- a/src/js/plyr.d.ts
+++ b/src/js/plyr.d.ts
@@ -214,26 +214,17 @@ declare class Plyr {
/**
* Add an event listener for the specified event.
*/
- on(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Add an event listener for the specified event once.
*/
- once(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Remove an event listener for the specified event.
*/
- off(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Check support for a mime type.
@@ -249,37 +240,51 @@ declare class Plyr {
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
- type StandardEvent =
- | 'progress'
- | 'playing'
- | 'play'
- | 'pause'
- | 'timeupdate'
- | 'volumechange'
- | 'seeking'
- | 'seeked'
- | 'ratechange'
- | 'ended'
- | 'enterfullscreen'
- | 'exitfullscreen'
- | 'captionsenabled'
- | 'captionsdisabled'
- | 'languagechange'
- | 'controlshidden'
- | 'controlsshown'
- | 'ready';
- type Html5Event =
- | 'loadstart'
- | 'loadeddata'
- | 'loadedmetadata'
- | 'canplay'
- | 'canplaythrough'
- | 'stalled'
- | 'waiting'
- | 'emptied'
- | 'cuechange'
- | 'error';
- type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
+ type StandardEventMap = {
+ progress: PlyrEvent;
+ playing: PlyrEvent;
+ play: PlyrEvent;
+ pause: PlyrEvent;
+ timeupdate: PlyrEvent;
+ volumechange: PlyrEvent;
+ seeking: PlyrEvent;
+ seeked: PlyrEvent;
+ ratechange: PlyrEvent;
+ ended: PlyrEvent;
+ enterfullscreen: PlyrEvent;
+ exitfullscreen: PlyrEvent;
+ captionsenabled: PlyrEvent;
+ captionsdisabled: PlyrEvent;
+ languagechange: PlyrEvent;
+ controlshidden: PlyrEvent;
+ controlsshown: PlyrEvent;
+ ready: PlyrEvent;
+ };
+ // For retrocompatibility, we keep StandadEvent
+ type StandadEvent = keyof Plyr.StandardEventMap;
+ type Html5EventMap = {
+ loadstart: PlyrEvent;
+ loadeddata: PlyrEvent;
+ loadedmetadata: PlyrEvent;
+ canplay: PlyrEvent;
+ canplaythrough: PlyrEvent;
+ stalled: PlyrEvent;
+ waiting: PlyrEvent;
+ emptied: PlyrEvent;
+ cuechange: PlyrEvent;
+ error: PlyrEvent;
+ };
+ // For retrocompatibility, we keep Html5Event
+ type Html5Event = keyof Plyr.Html5EventMap;
+ type YoutubeEventMap = {
+ statechange: PlyrStateChangeEvent;
+ qualitychange: PlyrEvent;
+ qualityrequested: PlyrEvent;
+ };
+ // For retrocompatibility, we keep YoutubeEvent
+ type YoutubeEvent = keyof Plyr.YoutubeEventMap;
+
+ type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;
interface FullscreenControl {
/**
@@ -552,7 +557,7 @@ declare namespace Plyr {
interface PreviewThumbnailsOptions {
enabled?: boolean;
- src?: string;
+ src?: string | string[];
}
interface SourceInfo {
@@ -623,6 +628,22 @@ declare namespace Plyr {
readonly detail: { readonly plyr: Plyr };
}
+ enum YoutubeState {
+ UNSTARTED = -1,
+ ENDED = 0,
+ PLAYING = 1,
+ PAUSED = 2,
+ BUFFERING = 3,
+ CUED = 5,
+ }
+
+ interface PlyrStateChangeEvent extends CustomEvent {
+ readonly detail: {
+ readonly plyr: Plyr;
+ readonly code: YoutubeState;
+ };
+ }
+
interface Support {
api: boolean;
ui: boolean;
diff --git a/src/js/plyr.js b/src/js/plyr.js
index ff92d95e..f1dcc68a 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -12,6 +12,7 @@ import { getProviderByUrl, providers, types } from './config/types';
import Console from './console';
import controls from './controls';
import Fullscreen from './fullscreen';
+import html5 from './html5';
import Listeners from './listeners';
import media from './media';
import Ads from './plugins/ads';
@@ -281,7 +282,7 @@ class Plyr {
// Listen for events if debugging
if (this.config.debug) {
- on.call(this, this.elements.container, this.config.events.join(' '), event => {
+ on.call(this, this.elements.container, this.config.events.join(' '), (event) => {
this.debug.log(`event: ${event.type}`);
});
}
@@ -308,7 +309,7 @@ class Plyr {
// Autoplay if required
if (this.isHTML5 && this.config.autoplay) {
- setTimeout(() => silencePromise(this.play()), 10);
+ this.once('canplay', () => silencePromise(this.play()));
}
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -1054,7 +1055,12 @@ class Plyr {
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// Close menu
- if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
+ if (
+ hiding &&
+ is.array(this.config.controls) &&
+ this.config.controls.includes('settings') &&
+ !is.empty(this.config.settings)
+ ) {
controls.toggleMenu.call(this, false);
}
@@ -1140,6 +1146,9 @@ class Plyr {
// Unbind listeners
unbindListeners.call(this);
+ // Cancel current network requests
+ html5.cancelRequests.call(this);
+
// Replace the container with the original element provided
replaceElement(this.elements.original, this.elements.container);
@@ -1248,7 +1257,7 @@ class Plyr {
return null;
}
- return targets.map(t => new Plyr(t, options));
+ return targets.map((t) => new Plyr(t, options));
}
}
diff --git a/src/js/source.js b/src/js/source.js
index b9fc7732..a62edbba 100644
--- a/src/js/source.js
+++ b/src/js/source.js
@@ -20,7 +20,7 @@ const source = {
src: attributes,
});
} else if (is.array(attributes)) {
- attributes.forEach(attribute => {
+ attributes.forEach((attribute) => {
insertElement(type, this.media, attribute);
});
}
diff --git a/src/js/ui.js b/src/js/ui.js
index d3d6fd69..c8b19677 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -135,7 +135,7 @@ const ui = {
}
// If there's a play button, set label
- Array.from(this.elements.buttons.play || []).forEach(button => {
+ Array.from(this.elements.buttons.play || []).forEach((button) => {
button.setAttribute('aria-label', label);
});
@@ -172,13 +172,16 @@ const ui = {
// Set property synchronously to respect the call order
this.media.setAttribute('data-poster', poster);
+ // Show the poster
+ this.elements.poster.removeAttribute('hidden');
+
// Wait until ui is ready
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
- .catch(err => {
+ .catch((err) => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
@@ -214,7 +217,7 @@ const ui = {
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state
- Array.from(this.elements.buttons.play || []).forEach(target => {
+ Array.from(this.elements.buttons.play || []).forEach((target) => {
Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
});
@@ -270,8 +273,8 @@ const ui = {
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values({ ...this.media.style })
// We're only fussed about Plyr specific properties
- .filter(key => !is.empty(key) && key.startsWith('--plyr'))
- .forEach(key => {
+ .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
+ .forEach((key) => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js
index d9e7615e..b4ccf268 100644
--- a/src/js/utils/animation.js
+++ b/src/js/utils/animation.js
@@ -14,7 +14,7 @@ export const transitionEndEvent = (() => {
transition: 'transitionend',
};
- const type = Object.keys(events).find(event => element.style[event] !== undefined);
+ const type = Object.keys(events).find((event) => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js
index 1d13b701..e8d2e595 100644
--- a/src/js/utils/elements.js
+++ b/src/js/utils/elements.js
@@ -138,7 +138,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
const attributes = {};
const existing = extend({}, existingAttributes);
- sel.split(',').forEach(s => {
+ sel.split(',').forEach((s) => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
@@ -198,7 +198,7 @@ export function toggleHidden(element, hidden) {
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) {
if (is.nodeList(element)) {
- return Array.from(element).map(e => toggleClass(e, className, force));
+ return Array.from(element).map((e) => toggleClass(e, className, force));
}
if (is.element(element)) {
diff --git a/src/js/utils/events.js b/src/js/utils/events.js
index 235eb629..287129f1 100644
--- a/src/js/utils/events.js
+++ b/src/js/utils/events.js
@@ -50,7 +50,7 @@ export function toggleListener(element, event, callback, toggle = false, passive
}
// If a single node is passed, bind the event listener
- events.forEach(type => {
+ events.forEach((type) => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
@@ -100,7 +100,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Unbind all cached event listeners
export function unbindListeners() {
if (this && this.eventListeners) {
- this.eventListeners.forEach(item => {
+ this.eventListeners.forEach((item) => {
const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options);
});
@@ -111,7 +111,7 @@ export function unbindListeners() {
// Run method when / if player is ready
export function ready() {
- return new Promise(resolve =>
+ return new Promise((resolve) =>
this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),
).then(() => {});
}
diff --git a/src/js/utils/is.js b/src/js/utils/is.js
index 1cc33848..3bb50a00 100644
--- a/src/js/utils/is.js
+++ b/src/js/utils/is.js
@@ -2,31 +2,31 @@
// Type checking utils
// ==========================================================================
-const getConstructor = input => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
+const getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
-const isNullOrUndefined = input => input === null || typeof input === 'undefined';
-const isObject = input => getConstructor(input) === Object;
-const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
-const isString = input => getConstructor(input) === String;
-const isBoolean = input => getConstructor(input) === Boolean;
-const isFunction = input => getConstructor(input) === Function;
-const isArray = input => Array.isArray(input);
-const isWeakMap = input => instanceOf(input, WeakMap);
-const isNodeList = input => instanceOf(input, NodeList);
-const isElement = input => instanceOf(input, Element);
-const isTextNode = input => getConstructor(input) === Text;
-const isEvent = input => instanceOf(input, Event);
-const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
-const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
-const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
-const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
+const isNullOrUndefined = (input) => input === null || typeof input === 'undefined';
+const isObject = (input) => getConstructor(input) === Object;
+const isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);
+const isString = (input) => getConstructor(input) === String;
+const isBoolean = (input) => getConstructor(input) === Boolean;
+const isFunction = (input) => getConstructor(input) === Function;
+const isArray = (input) => Array.isArray(input);
+const isWeakMap = (input) => instanceOf(input, WeakMap);
+const isNodeList = (input) => instanceOf(input, NodeList);
+const isElement = (input) => instanceOf(input, Element);
+const isTextNode = (input) => getConstructor(input) === Text;
+const isEvent = (input) => instanceOf(input, Event);
+const isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);
+const isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
+const isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
+const isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);
-const isEmpty = input =>
+const isEmpty = (input) =>
isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length);
-const isUrl = input => {
+const isUrl = (input) => {
// Accept a URL object
if (instanceOf(input, window.URL)) {
return true;
diff --git a/src/js/utils/load-sprite.js b/src/js/utils/load-sprite.js
index 0a4eff99..293163e5 100644
--- a/src/js/utils/load-sprite.js
+++ b/src/js/utils/load-sprite.js
@@ -54,7 +54,7 @@ export default function loadSprite(url, id) {
// Get the sprite
fetch(url)
- .then(result => {
+ .then((result) => {
if (is.empty(result)) {
return;
}
diff --git a/src/js/utils/objects.js b/src/js/utils/objects.js
index a327e488..d64002ae 100644
--- a/src/js/utils/objects.js
+++ b/src/js/utils/objects.js
@@ -26,7 +26,7 @@ export function extend(target = {}, ...sources) {
return target;
}
- Object.keys(source).forEach(key => {
+ Object.keys(source).forEach((key) => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
diff --git a/src/js/utils/strings.js b/src/js/utils/strings.js
index b7de04c1..d4cc1efa 100644
--- a/src/js/utils/strings.js
+++ b/src/js/utils/strings.js
@@ -33,7 +33,7 @@ export const replaceAll = (input = '', find = '', replace = '') =>
// Convert to title case
export const toTitleCase = (input = '') =>
- input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
+ input.toString().replace(/\w\S*/g, (text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
// Convert string to pascalCase
export function toPascalCase(input = '') {
diff --git a/src/js/utils/style.js b/src/js/utils/style.js
index c2004fcb..f02b0ba5 100644
--- a/src/js/utils/style.js
+++ b/src/js/utils/style.js
@@ -27,7 +27,7 @@ export function reduceAspectRatio(ratio) {
}
export function getAspectRatio(input) {
- const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
+ const parse = (ratio) => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
// Try provided ratio
let ratio = parse(input);
@@ -68,7 +68,11 @@ export function setAspectRatio(input) {
const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
const offset = (height - padding) / (height / 50);
- this.media.style.transform = `translateY(-${offset}%)`;
+ if (this.fullscreen.active) {
+ wrapper.style.paddingBottom = null;
+ } else {
+ this.media.style.transform = `translateY(-${offset}%)`;
+ }
} else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
diff --git a/src/js/utils/time.js b/src/js/utils/time.js
index 31660c4a..36e0d59b 100644
--- a/src/js/utils/time.js
+++ b/src/js/utils/time.js
@@ -5,9 +5,9 @@
import is from './is';
// Time helpers
-export const getHours = value => Math.trunc((value / 60 / 60) % 60, 10);
-export const getMinutes = value => Math.trunc((value / 60) % 60, 10);
-export const getSeconds = value => Math.trunc(value % 60, 10);
+export const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);
+export const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);
+export const getSeconds = (value) => Math.trunc(value % 60, 10);
// Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) {
@@ -17,7 +17,7 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
}
// Format time component to add leading zero
- const format = value => `0${value}`.slice(-2);
+ const format = (value) => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);