This commit is contained in:
Sam Potts 2018-10-24 23:04:18 +11:00
parent 779e45c11b
commit e49da6c13f
17 changed files with 349 additions and 172 deletions

View File

@ -1,3 +1,10 @@
# v3.4.5
- Added download button option to download either current source or a custom URL you specify in options
- Prevent immediate hiding of controls on mobile (thanks @jamesoflol)
- Don't hide controls on focusout event (fixes #1122) (thanks @jamesoflol)
- Fix HTML5 quality settings being incorrectly set in local storage (thanks @TechGuard)
# v3.4.4 # v3.4.4
- Fixed issue with double binding for `click` and `touchstart` for `clickToPlay` option - Fixed issue with double binding for `click` and `touchstart` for `clickToPlay` option

View File

@ -28,6 +28,7 @@ controls: [
'settings', // Settings menu 'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only) 'pip', // Picture-in-picture (currently Safari only)
'airplay', // Airplay (currently Safari only) 'airplay', // Airplay (currently Safari only)
'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
'fullscreen', // Toggle fullscreen 'fullscreen', // Toggle fullscreen
]; ];
``` ```

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

230
dist/plyr.js vendored
View File

@ -178,6 +178,11 @@ typeof navigator === "object" && (function (global, factory) {
// Accept a URL object // Accept a URL object
if (instanceOf(input, window.URL)) { if (instanceOf(input, window.URL)) {
return true; return true;
} // Must be string from here
if (!isString(input)) {
return false;
} // Add the protocol if required } // Add the protocol if required
@ -1006,6 +1011,13 @@ typeof navigator === "object" && (function (global, factory) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
var resources = {
pip: 'PIP',
airplay: 'AirPlay',
html5: 'HTML5',
vimeo: 'Vimeo',
youtube: 'YouTube'
};
var i18n = { var i18n = {
get: function get() { get: function get() {
var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@ -1018,6 +1030,10 @@ typeof navigator === "object" && (function (global, factory) {
var string = getDeep(config.i18n, key); var string = getDeep(config.i18n, key);
if (is.empty(string)) { if (is.empty(string)) {
if (Object.keys(resources).includes(key)) {
return resources[key];
}
return ''; return '';
} }
@ -1330,23 +1346,18 @@ typeof navigator === "object" && (function (global, factory) {
if ('href' in use) { if ('href' in use) {
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
} else { } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
} // Add <use> to <svg>
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
icon.appendChild(use); icon.appendChild(use);
return icon; return icon;
}, },
// Create hidden text label // Create hidden text label
createLabel: function createLabel(type) { createLabel: function createLabel(key) {
var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Skip i18n for abbreviations and brand names var text = i18n.get(key, this.config);
var universals = {
pip: 'PIP',
airplay: 'AirPlay'
};
var text = universals[type] || i18n.get(type, this.config);
var attributes = Object.assign({}, attr, { var attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
}); });
@ -1368,20 +1379,29 @@ typeof navigator === "object" && (function (global, factory) {
}, },
// Create a <button> // Create a <button>
createButton: function createButton(buttonType, attr) { createButton: function createButton(buttonType, attr) {
var button = createElement('button');
var attributes = Object.assign({}, attr); var attributes = Object.assign({}, attr);
var type = toCamelCase(buttonType); var type = toCamelCase(buttonType);
var toggle = false; var props = {
var label; element: 'button',
var icon; toggle: false,
var labelPressed; label: null,
var iconPressed; icon: null,
labelPressed: null,
if (!('type' in attributes)) { iconPressed: null
attributes.type = 'button'; };
['element', 'icon', 'label'].forEach(function (key) {
if (Object.keys(attributes).includes(key)) {
props[key] = attributes[key];
delete attributes[key];
} }
}); // Default to 'button' type to prevent form submission
if ('class' in attributes) { if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
attributes.type = 'button';
} // Set class name
if (Object.keys(attributes).includes('class')) {
if (!attributes.class.includes(this.config.classNames.control)) { if (!attributes.class.includes(this.config.classNames.control)) {
attributes.class += " ".concat(this.config.classNames.control); attributes.class += " ".concat(this.config.classNames.control);
} }
@ -1392,69 +1412,76 @@ typeof navigator === "object" && (function (global, factory) {
switch (buttonType) { switch (buttonType) {
case 'play': case 'play':
toggle = true; props.toggle = true;
label = 'play'; props.label = 'play';
labelPressed = 'pause'; props.labelPressed = 'pause';
icon = 'play'; props.icon = 'play';
iconPressed = 'pause'; props.iconPressed = 'pause';
break; break;
case 'mute': case 'mute':
toggle = true; props.toggle = true;
label = 'mute'; props.label = 'mute';
labelPressed = 'unmute'; props.labelPressed = 'unmute';
icon = 'volume'; props.icon = 'volume';
iconPressed = 'muted'; props.iconPressed = 'muted';
break; break;
case 'captions': case 'captions':
toggle = true; props.toggle = true;
label = 'enableCaptions'; props.label = 'enableCaptions';
labelPressed = 'disableCaptions'; props.labelPressed = 'disableCaptions';
icon = 'captions-off'; props.icon = 'captions-off';
iconPressed = 'captions-on'; props.iconPressed = 'captions-on';
break; break;
case 'fullscreen': case 'fullscreen':
toggle = true; props.toggle = true;
label = 'enterFullscreen'; props.label = 'enterFullscreen';
labelPressed = 'exitFullscreen'; props.labelPressed = 'exitFullscreen';
icon = 'enter-fullscreen'; props.icon = 'enter-fullscreen';
iconPressed = 'exit-fullscreen'; props.iconPressed = 'exit-fullscreen';
break; break;
case 'play-large': case 'play-large':
attributes.class += " ".concat(this.config.classNames.control, "--overlaid"); attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
type = 'play'; type = 'play';
label = 'play'; props.label = 'play';
icon = 'play'; props.icon = 'play';
break; break;
default: default:
label = type; if (is.empty(props.label)) {
icon = buttonType; props.label = type;
} // Setup toggle icon and labels }
if (is.empty(props.icon)) {
props.icon = buttonType;
}
if (toggle) { }
var button = createElement(props.element); // Setup toggle icon and labels
if (props.toggle) {
// Icon // Icon
button.appendChild(controls.createIcon.call(this, iconPressed, { button.appendChild(controls.createIcon.call(this, props.iconPressed, {
class: 'icon--pressed' class: 'icon--pressed'
})); }));
button.appendChild(controls.createIcon.call(this, icon, { button.appendChild(controls.createIcon.call(this, props.icon, {
class: 'icon--not-pressed' class: 'icon--not-pressed'
})); // Label/Tooltip })); // Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { button.appendChild(controls.createLabel.call(this, props.labelPressed, {
class: 'label--pressed' class: 'label--pressed'
})); }));
button.appendChild(controls.createLabel.call(this, label, { button.appendChild(controls.createLabel.call(this, props.label, {
class: 'label--not-pressed' class: 'label--not-pressed'
})); }));
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, props.icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, props.label));
} // Merge attributes } // Merge and set attributes
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
@ -2307,6 +2334,17 @@ typeof navigator === "object" && (function (global, factory) {
controls.focusFirstMenuItem.call(this, target, tabFocus); controls.focusFirstMenuItem.call(this, target, tabFocus);
}, },
// Set the download link
setDownloadLink: function setDownloadLink() {
var button = this.elements.buttons.download; // Bail if no button
if (!is.element(button)) {
return;
} // Set download link
button.setAttribute('href', this.download);
},
// Build the default HTML // Build the default HTML
// TODO: Set order based on order in the config.controls array? // TODO: Set order based on order in the config.controls array?
create: function create(data) { create: function create(data) {
@ -2512,6 +2550,25 @@ typeof navigator === "object" && (function (global, factory) {
if (this.config.controls.includes('airplay') && support.airplay) { if (this.config.controls.includes('airplay') && support.airplay) {
container.appendChild(controls.createButton.call(this, 'airplay')); container.appendChild(controls.createButton.call(this, 'airplay'));
} // Download button
if (this.config.controls.includes('download')) {
var _attributes = {
element: 'a',
href: this.download,
target: '_blank'
};
var download = this.config.urls.download;
if (!is.url(download) && this.isEmbed) {
extend(_attributes, {
icon: "logo-".concat(this.provider),
label: this.provider
});
}
container.appendChild(controls.createButton.call(this, 'download', _attributes));
} // Toggle fullscreen button } // Toggle fullscreen button
@ -3178,7 +3235,8 @@ typeof navigator === "object" && (function (global, factory) {
controls: ['play-large', // 'restart', controls: ['play-large', // 'restart',
// 'rewind', // 'rewind',
'play', // 'fast-forward', 'play', // 'fast-forward',
'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
'fullscreen'],
settings: ['captions', 'quality', 'speed'], settings: ['captions', 'quality', 'speed'],
// Localisation // Localisation
i18n: { i18n: {
@ -3198,6 +3256,7 @@ typeof navigator === "object" && (function (global, factory) {
unmute: 'Unmute', unmute: 'Unmute',
enableCaptions: 'Enable captions', enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions', disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen', enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen', exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}', frameTitle: 'Player for {title}',
@ -3226,6 +3285,7 @@ typeof navigator === "object" && (function (global, factory) {
}, },
// URLs // URLs
urls: { urls: {
download: null,
vimeo: { vimeo: {
sdk: 'https://player.vimeo.com/api/player.js', sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}', iframe: 'https://player.vimeo.com/video/{0}?{1}',
@ -3250,6 +3310,7 @@ typeof navigator === "object" && (function (global, factory) {
mute: null, mute: null,
volume: null, volume: null,
captions: null, captions: null,
download: null,
fullscreen: null, fullscreen: null,
pip: null, pip: null,
airplay: null, airplay: null,
@ -3262,7 +3323,7 @@ typeof navigator === "object" && (function (global, factory) {
events: [// Events to watch on HTML5 media elements and bubble events: [// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
'statechange', // Quality 'statechange', // Quality
'qualitychange', // Ads 'qualitychange', // Ads
'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
@ -3284,6 +3345,7 @@ typeof navigator === "object" && (function (global, factory) {
fastForward: '[data-plyr="fast-forward"]', fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]', mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]', captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]', fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]', pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]', airplay: '[data-plyr="airplay"]',
@ -3396,7 +3458,7 @@ typeof navigator === "object" && (function (global, factory) {
}; };
/** /**
* Get provider by URL * Get provider by URL
* @param {string} url * @param {String} url
*/ */
function getProviderByUrl(url) { function getProviderByUrl(url) {
@ -3923,8 +3985,10 @@ typeof navigator === "object" && (function (global, factory) {
var controls$$1 = this.elements.controls; var controls$$1 = this.elements.controls;
if (controls$$1 && this.config.hideControls) { if (controls$$1 && this.config.hideControls) {
// Show controls if force, loading, paused, or button interaction, otherwise hide // 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.)
this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover)); var 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$$1.pressed || controls$$1.hover || recentTouchSeek));
} }
} }
}; };
@ -4283,7 +4347,7 @@ typeof navigator === "object" && (function (global, factory) {
if (!is.element(wrapper)) { if (!is.element(wrapper)) {
return; return;
} // On click play, pause ore restart } // On click play, pause or restart
on.call(player, elements.container, 'click', function (event) { on.call(player, elements.container, 'click', function (event) {
@ -4336,6 +4400,10 @@ typeof navigator === "object" && (function (global, factory) {
on.call(player, player.media, 'qualitychange', function (event) { on.call(player, player.media, 'qualitychange', function (event) {
// Update UI // Update UI
controls.updateSetting.call(player, 'quality', null, event.detail.quality); controls.updateSetting.call(player, 'quality', null, event.detail.quality);
}); // Update download link when ready and if quality changes
on.call(player, player.media, 'ready qualitychange', function () {
controls.setDownloadLink.call(player);
}); // Proxy events to container }); // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
@ -4413,7 +4481,11 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.buttons.captions, 'click', function () { this.bind(elements.buttons.captions, 'click', function () {
return player.toggleCaptions(); return player.toggleCaptions();
}); // Fullscreen toggle }); // Download
this.bind(elements.buttons.download, 'click', function () {
triggerEvent.call(player, player.media, 'download');
}, 'download'); // Fullscreen toggle
this.bind(elements.buttons.fullscreen, 'click', function () { this.bind(elements.buttons.fullscreen, 'click', function () {
player.fullscreen.toggle(); player.fullscreen.toggle();
@ -4476,9 +4548,11 @@ typeof navigator === "object" && (function (global, factory) {
if (is.keyboardEvent(event) && code !== 39 && code !== 37) { if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
return; return;
} // Was playing before? } // Record seek time so we can prevent hiding controls for a few seconds after seek
player.lastSeekTime = Date.now(); // Was playing before?
var play = seek.hasAttribute(attribute); // Done seeking var play = seek.hasAttribute(attribute); // Done seeking
var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
@ -4555,32 +4629,28 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
}); // Focus in/out on controls }); // Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin focusout', function (event) { this.bind(elements.controls, 'focusin', function () {
var config = player.config, var config = player.config,
elements = player.elements, elements = player.elements,
timers = player.timers; timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay ui.toggleControls.call(player, true); // Restore transition
if (isFocusIn) {
// Restore transition
setTimeout(function () { setTimeout(function () {
toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0); // Delay a little more for keyboard users }, 0); // Delay a little more for mouse users
var delay = _this2.touch ? 3000 : 4000; // Clear timer var delay = _this2.touch ? 3000 : 4000; // Clear timer
clearTimeout(timers.controls); // Hide clearTimeout(timers.controls); // Hide again after delay
timers.controls = setTimeout(function () { timers.controls = setTimeout(function () {
return ui.toggleControls.call(player, false); return ui.toggleControls.call(player, false);
}, delay); }, delay);
}
}); // Mouse wheel for volume }); // Mouse wheel for volume
this.bind(elements.inputs.volume, 'wheel', function (event) { this.bind(elements.inputs.volume, 'wheel', function (event) {
@ -5171,6 +5241,7 @@ typeof navigator === "object" && (function (global, factory) {
var currentSrc; var currentSrc;
player.embed.getVideoUrl().then(function (value) { player.embed.getVideoUrl().then(function (value) {
currentSrc = value; currentSrc = value;
controls.setDownloadLink.call(player);
}).catch(function (error) { }).catch(function (error) {
_this2.debug.warn(error); _this2.debug.warn(error);
}); });
@ -6724,7 +6795,10 @@ typeof navigator === "object" && (function (global, factory) {
if (this.config.autoplay) { if (this.config.autoplay) {
this.play(); this.play();
} } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
this.lastSeekTime = 0;
} // --------------------------------------- } // ---------------------------------------
// API // API
// --------------------------------------- // ---------------------------------------
@ -7448,6 +7522,16 @@ typeof navigator === "object" && (function (global, factory) {
get: function get() { get: function get() {
return this.media.currentSrc; return this.media.currentSrc;
} }
/**
* Get a download URL (either source or custom)
*/
}, {
key: "download",
get: function get() {
var download = this.config.urls.download;
return is.url(download) ? download : this.source;
}
/** /**
* Set the poster image for a video * Set the poster image for a video
* @param {input} - the URL for the new poster image * @param {input} - the URL for the new poster image

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2804,6 +2804,11 @@ typeof navigator === "object" && (function (global, factory) {
// Accept a URL object // Accept a URL object
if (instanceOf(input, window.URL)) { if (instanceOf(input, window.URL)) {
return true; return true;
} // Must be string from here
if (!isString(input)) {
return false;
} // Add the protocol if required } // Add the protocol if required
@ -3669,6 +3674,13 @@ typeof navigator === "object" && (function (global, factory) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
var resources = {
pip: 'PIP',
airplay: 'AirPlay',
html5: 'HTML5',
vimeo: 'Vimeo',
youtube: 'YouTube'
};
var i18n = { var i18n = {
get: function get() { get: function get() {
var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@ -3681,6 +3693,10 @@ typeof navigator === "object" && (function (global, factory) {
var string = getDeep(config.i18n, key); var string = getDeep(config.i18n, key);
if (is$1.empty(string)) { if (is$1.empty(string)) {
if (Object.keys(resources).includes(key)) {
return resources[key];
}
return ''; return '';
} }
@ -3993,23 +4009,18 @@ typeof navigator === "object" && (function (global, factory) {
if ('href' in use) { if ('href' in use) {
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
} else { } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
} // Add <use> to <svg>
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
icon.appendChild(use); icon.appendChild(use);
return icon; return icon;
}, },
// Create hidden text label // Create hidden text label
createLabel: function createLabel(type) { createLabel: function createLabel(key) {
var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Skip i18n for abbreviations and brand names var text = i18n.get(key, this.config);
var universals = {
pip: 'PIP',
airplay: 'AirPlay'
};
var text = universals[type] || i18n.get(type, this.config);
var attributes = Object.assign({}, attr, { var attributes = Object.assign({}, attr, {
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
}); });
@ -4031,20 +4042,29 @@ typeof navigator === "object" && (function (global, factory) {
}, },
// Create a <button> // Create a <button>
createButton: function createButton(buttonType, attr) { createButton: function createButton(buttonType, attr) {
var button = createElement('button');
var attributes = Object.assign({}, attr); var attributes = Object.assign({}, attr);
var type = toCamelCase(buttonType); var type = toCamelCase(buttonType);
var toggle = false; var props = {
var label; element: 'button',
var icon; toggle: false,
var labelPressed; label: null,
var iconPressed; icon: null,
labelPressed: null,
if (!('type' in attributes)) { iconPressed: null
attributes.type = 'button'; };
['element', 'icon', 'label'].forEach(function (key) {
if (Object.keys(attributes).includes(key)) {
props[key] = attributes[key];
delete attributes[key];
} }
}); // Default to 'button' type to prevent form submission
if ('class' in attributes) { if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
attributes.type = 'button';
} // Set class name
if (Object.keys(attributes).includes('class')) {
if (!attributes.class.includes(this.config.classNames.control)) { if (!attributes.class.includes(this.config.classNames.control)) {
attributes.class += " ".concat(this.config.classNames.control); attributes.class += " ".concat(this.config.classNames.control);
} }
@ -4055,69 +4075,76 @@ typeof navigator === "object" && (function (global, factory) {
switch (buttonType) { switch (buttonType) {
case 'play': case 'play':
toggle = true; props.toggle = true;
label = 'play'; props.label = 'play';
labelPressed = 'pause'; props.labelPressed = 'pause';
icon = 'play'; props.icon = 'play';
iconPressed = 'pause'; props.iconPressed = 'pause';
break; break;
case 'mute': case 'mute':
toggle = true; props.toggle = true;
label = 'mute'; props.label = 'mute';
labelPressed = 'unmute'; props.labelPressed = 'unmute';
icon = 'volume'; props.icon = 'volume';
iconPressed = 'muted'; props.iconPressed = 'muted';
break; break;
case 'captions': case 'captions':
toggle = true; props.toggle = true;
label = 'enableCaptions'; props.label = 'enableCaptions';
labelPressed = 'disableCaptions'; props.labelPressed = 'disableCaptions';
icon = 'captions-off'; props.icon = 'captions-off';
iconPressed = 'captions-on'; props.iconPressed = 'captions-on';
break; break;
case 'fullscreen': case 'fullscreen':
toggle = true; props.toggle = true;
label = 'enterFullscreen'; props.label = 'enterFullscreen';
labelPressed = 'exitFullscreen'; props.labelPressed = 'exitFullscreen';
icon = 'enter-fullscreen'; props.icon = 'enter-fullscreen';
iconPressed = 'exit-fullscreen'; props.iconPressed = 'exit-fullscreen';
break; break;
case 'play-large': case 'play-large':
attributes.class += " ".concat(this.config.classNames.control, "--overlaid"); attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
type = 'play'; type = 'play';
label = 'play'; props.label = 'play';
icon = 'play'; props.icon = 'play';
break; break;
default: default:
label = type; if (is$1.empty(props.label)) {
icon = buttonType; props.label = type;
} // Setup toggle icon and labels }
if (is$1.empty(props.icon)) {
props.icon = buttonType;
}
if (toggle) { }
var button = createElement(props.element); // Setup toggle icon and labels
if (props.toggle) {
// Icon // Icon
button.appendChild(controls.createIcon.call(this, iconPressed, { button.appendChild(controls.createIcon.call(this, props.iconPressed, {
class: 'icon--pressed' class: 'icon--pressed'
})); }));
button.appendChild(controls.createIcon.call(this, icon, { button.appendChild(controls.createIcon.call(this, props.icon, {
class: 'icon--not-pressed' class: 'icon--not-pressed'
})); // Label/Tooltip })); // Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { button.appendChild(controls.createLabel.call(this, props.labelPressed, {
class: 'label--pressed' class: 'label--pressed'
})); }));
button.appendChild(controls.createLabel.call(this, label, { button.appendChild(controls.createLabel.call(this, props.label, {
class: 'label--not-pressed' class: 'label--not-pressed'
})); }));
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, props.icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, props.label));
} // Merge attributes } // Merge and set attributes
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
@ -4970,6 +4997,17 @@ typeof navigator === "object" && (function (global, factory) {
controls.focusFirstMenuItem.call(this, target, tabFocus); controls.focusFirstMenuItem.call(this, target, tabFocus);
}, },
// Set the download link
setDownloadLink: function setDownloadLink() {
var button = this.elements.buttons.download; // Bail if no button
if (!is$1.element(button)) {
return;
} // Set download link
button.setAttribute('href', this.download);
},
// Build the default HTML // Build the default HTML
// TODO: Set order based on order in the config.controls array? // TODO: Set order based on order in the config.controls array?
create: function create(data) { create: function create(data) {
@ -5175,6 +5213,25 @@ typeof navigator === "object" && (function (global, factory) {
if (this.config.controls.includes('airplay') && support.airplay) { if (this.config.controls.includes('airplay') && support.airplay) {
container.appendChild(controls.createButton.call(this, 'airplay')); container.appendChild(controls.createButton.call(this, 'airplay'));
} // Download button
if (this.config.controls.includes('download')) {
var _attributes = {
element: 'a',
href: this.download,
target: '_blank'
};
var download = this.config.urls.download;
if (!is$1.url(download) && this.isEmbed) {
extend(_attributes, {
icon: "logo-".concat(this.provider),
label: this.provider
});
}
container.appendChild(controls.createButton.call(this, 'download', _attributes));
} // Toggle fullscreen button } // Toggle fullscreen button
@ -5841,7 +5898,8 @@ typeof navigator === "object" && (function (global, factory) {
controls: ['play-large', // 'restart', controls: ['play-large', // 'restart',
// 'rewind', // 'rewind',
'play', // 'fast-forward', 'play', // 'fast-forward',
'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
'fullscreen'],
settings: ['captions', 'quality', 'speed'], settings: ['captions', 'quality', 'speed'],
// Localisation // Localisation
i18n: { i18n: {
@ -5861,6 +5919,7 @@ typeof navigator === "object" && (function (global, factory) {
unmute: 'Unmute', unmute: 'Unmute',
enableCaptions: 'Enable captions', enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions', disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen', enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen', exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}', frameTitle: 'Player for {title}',
@ -5889,6 +5948,7 @@ typeof navigator === "object" && (function (global, factory) {
}, },
// URLs // URLs
urls: { urls: {
download: null,
vimeo: { vimeo: {
sdk: 'https://player.vimeo.com/api/player.js', sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}', iframe: 'https://player.vimeo.com/video/{0}?{1}',
@ -5913,6 +5973,7 @@ typeof navigator === "object" && (function (global, factory) {
mute: null, mute: null,
volume: null, volume: null,
captions: null, captions: null,
download: null,
fullscreen: null, fullscreen: null,
pip: null, pip: null,
airplay: null, airplay: null,
@ -5925,7 +5986,7 @@ typeof navigator === "object" && (function (global, factory) {
events: [// Events to watch on HTML5 media elements and bubble events: [// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
'statechange', // Quality 'statechange', // Quality
'qualitychange', // Ads 'qualitychange', // Ads
'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
@ -5947,6 +6008,7 @@ typeof navigator === "object" && (function (global, factory) {
fastForward: '[data-plyr="fast-forward"]', fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]', mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]', captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]', fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]', pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]', airplay: '[data-plyr="airplay"]',
@ -6059,7 +6121,7 @@ typeof navigator === "object" && (function (global, factory) {
}; };
/** /**
* Get provider by URL * Get provider by URL
* @param {string} url * @param {String} url
*/ */
function getProviderByUrl(url) { function getProviderByUrl(url) {
@ -6596,8 +6658,10 @@ typeof navigator === "object" && (function (global, factory) {
var controls$$1 = this.elements.controls; var controls$$1 = this.elements.controls;
if (controls$$1 && this.config.hideControls) { if (controls$$1 && this.config.hideControls) {
// Show controls if force, loading, paused, or button interaction, otherwise hide // 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.)
this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover)); var 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$$1.pressed || controls$$1.hover || recentTouchSeek));
} }
} }
}; };
@ -6956,7 +7020,7 @@ typeof navigator === "object" && (function (global, factory) {
if (!is$1.element(wrapper)) { if (!is$1.element(wrapper)) {
return; return;
} // On click play, pause ore restart } // On click play, pause or restart
on.call(player, elements.container, 'click', function (event) { on.call(player, elements.container, 'click', function (event) {
@ -7009,6 +7073,10 @@ typeof navigator === "object" && (function (global, factory) {
on.call(player, player.media, 'qualitychange', function (event) { on.call(player, player.media, 'qualitychange', function (event) {
// Update UI // Update UI
controls.updateSetting.call(player, 'quality', null, event.detail.quality); controls.updateSetting.call(player, 'quality', null, event.detail.quality);
}); // Update download link when ready and if quality changes
on.call(player, player.media, 'ready qualitychange', function () {
controls.setDownloadLink.call(player);
}); // Proxy events to container }); // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
@ -7086,7 +7154,11 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.buttons.captions, 'click', function () { this.bind(elements.buttons.captions, 'click', function () {
return player.toggleCaptions(); return player.toggleCaptions();
}); // Fullscreen toggle }); // Download
this.bind(elements.buttons.download, 'click', function () {
triggerEvent.call(player, player.media, 'download');
}, 'download'); // Fullscreen toggle
this.bind(elements.buttons.fullscreen, 'click', function () { this.bind(elements.buttons.fullscreen, 'click', function () {
player.fullscreen.toggle(); player.fullscreen.toggle();
@ -7149,9 +7221,11 @@ typeof navigator === "object" && (function (global, factory) {
if (is$1.keyboardEvent(event) && code !== 39 && code !== 37) { if (is$1.keyboardEvent(event) && code !== 39 && code !== 37) {
return; return;
} // Was playing before? } // Record seek time so we can prevent hiding controls for a few seconds after seek
player.lastSeekTime = Date.now(); // Was playing before?
var play = seek.hasAttribute(attribute); // Done seeking var play = seek.hasAttribute(attribute); // Done seeking
var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
@ -7228,32 +7302,28 @@ typeof navigator === "object" && (function (global, factory) {
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
}); // Focus in/out on controls }); // Show controls when they receive focus (e.g., when using keyboard tab key)
this.bind(elements.controls, 'focusin focusout', function (event) { this.bind(elements.controls, 'focusin', function () {
var config = player.config, var config = player.config,
elements = player.elements, elements = player.elements,
timers = player.timers; timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay ui.toggleControls.call(player, true); // Restore transition
if (isFocusIn) {
// Restore transition
setTimeout(function () { setTimeout(function () {
toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0); // Delay a little more for keyboard users }, 0); // Delay a little more for mouse users
var delay = _this2.touch ? 3000 : 4000; // Clear timer var delay = _this2.touch ? 3000 : 4000; // Clear timer
clearTimeout(timers.controls); // Hide clearTimeout(timers.controls); // Hide again after delay
timers.controls = setTimeout(function () { timers.controls = setTimeout(function () {
return ui.toggleControls.call(player, false); return ui.toggleControls.call(player, false);
}, delay); }, delay);
}
}); // Mouse wheel for volume }); // Mouse wheel for volume
this.bind(elements.inputs.volume, 'wheel', function (event) { this.bind(elements.inputs.volume, 'wheel', function (event) {
@ -7864,6 +7934,7 @@ typeof navigator === "object" && (function (global, factory) {
var currentSrc; var currentSrc;
player.embed.getVideoUrl().then(function (value) { player.embed.getVideoUrl().then(function (value) {
currentSrc = value; currentSrc = value;
controls.setDownloadLink.call(player);
}).catch(function (error) { }).catch(function (error) {
_this2.debug.warn(error); _this2.debug.warn(error);
}); });
@ -9414,7 +9485,10 @@ typeof navigator === "object" && (function (global, factory) {
if (this.config.autoplay) { if (this.config.autoplay) {
this.play(); this.play();
} } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
this.lastSeekTime = 0;
} // --------------------------------------- } // ---------------------------------------
// API // API
// --------------------------------------- // ---------------------------------------
@ -10138,6 +10212,16 @@ typeof navigator === "object" && (function (global, factory) {
get: function get() { get: function get() {
return this.media.currentSrc; return this.media.currentSrc;
} }
/**
* Get a download URL (either source or custom)
*/
}, {
key: "download",
get: function get() {
var download = this.config.urls.download;
return is$1.url(download) ? download : this.source;
}
/** /**
* Set the poster image for a video * Set the poster image for a video
* @param {input} - the URL for the new poster image * @param {input} - the URL for the new poster image

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/plyr.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.4.4", "version": "3.4.5",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",

View File

@ -132,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build. You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.4.4/plyr.js"></script> <script src="https://cdn.plyr.io/3.4.5/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.4.4/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.4.5/plyr.polyfilled.js"></script>
``` ```
### CSS ### CSS
@ -152,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.4.4/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.4.5/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.4/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.5/plyr.svg`.
## Ads ## Ads
@ -315,6 +315,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. | | `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. | | `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. | | `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
| `urls` | Object | See source. | If you wish to override any API URLs then you can do so here. You can also set a custom download URL for the download button. |
1. Vimeo only 1. Vimeo only

View File

@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.4.4 // plyr.js v3.4.5
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================

View File

@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.4.4 // plyr.js v3.4.5
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================