Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
1a9b860e68 | |||
cede7d0f35 | |||
fe26d383f1 | |||
df4bc268dc | |||
e49da6c13f | |||
67b7262764 | |||
88528ef979 | |||
b6175b1ca9 | |||
aa20ebaa9c | |||
779e45c11b | |||
5d5a6eabaa | |||
03c9b53232 | |||
ebaded66b4 | |||
c232eb2478 | |||
7559cc6897 | |||
69d0d6d7ee | |||
3e9336b15d | |||
5c78ecc15d | |||
06db3f702d | |||
a2a82a96a6 | |||
a86bbae851 | |||
fac134dd95 | |||
515ae32160 | |||
df8f040795 | |||
64a23992f0 | |||
f5baff6e6b | |||
8bdd90a2a8 | |||
5536e97482 |
12
changelog.md
@ -1,3 +1,15 @@
|
||||
# v3.4.6
|
||||
|
||||
- Added picture-in-picture support for Chrome 70+
|
||||
- Fixed issue with versioning the SVG sprite in the gulp build script
|
||||
|
||||
# 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
|
||||
|
||||
- Fixed issue with double binding for `click` and `touchstart` for `clickToPlay` option
|
||||
|
@ -28,6 +28,7 @@ controls: [
|
||||
'settings', // Settings menu
|
||||
'pip', // Picture-in-picture (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
|
||||
];
|
||||
```
|
||||
|
2
demo/dist/demo.css
vendored
2
dist/plyr.css
vendored
319
dist/plyr.js
vendored
@ -178,6 +178,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
// Accept a URL object
|
||||
if (instanceOf(input, window.URL)) {
|
||||
return true;
|
||||
} // Must be string from here
|
||||
|
||||
|
||||
if (!isString(input)) {
|
||||
return false;
|
||||
} // Add the protocol if required
|
||||
|
||||
|
||||
@ -700,9 +705,25 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
};
|
||||
},
|
||||
// Picture-in-picture support
|
||||
// Safari only currently
|
||||
// Safari & Chrome only currently
|
||||
pip: function () {
|
||||
return !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode);
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
} // Safari
|
||||
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
|
||||
|
||||
|
||||
if (is.function(createElement('video').webkitSetPresentationMode)) {
|
||||
return true;
|
||||
} // Chrome
|
||||
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
|
||||
|
||||
|
||||
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}(),
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
@ -835,10 +856,6 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input
|
||||
}); // Save to storage
|
||||
|
||||
player.storage.set({
|
||||
quality: input
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1006,6 +1023,13 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
var resources = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay',
|
||||
html5: 'HTML5',
|
||||
vimeo: 'Vimeo',
|
||||
youtube: 'YouTube'
|
||||
};
|
||||
var i18n = {
|
||||
get: function get() {
|
||||
var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
||||
@ -1018,6 +1042,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var string = getDeep(config.i18n, key);
|
||||
|
||||
if (is.empty(string)) {
|
||||
if (Object.keys(resources).includes(key)) {
|
||||
return resources[key];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -1330,23 +1358,18 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if ('href' in use) {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
|
||||
} else {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
|
||||
} // Add <use> to <svg>
|
||||
} // 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>
|
||||
|
||||
icon.appendChild(use);
|
||||
return icon;
|
||||
},
|
||||
// Create hidden text label
|
||||
createLabel: function createLabel(type) {
|
||||
createLabel: function createLabel(key) {
|
||||
var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
// Skip i18n for abbreviations and brand names
|
||||
var universals = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay'
|
||||
};
|
||||
var text = universals[type] || i18n.get(type, this.config);
|
||||
var text = i18n.get(key, this.config);
|
||||
var attributes = Object.assign({}, attr, {
|
||||
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
|
||||
});
|
||||
@ -1368,20 +1391,29 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
},
|
||||
// Create a <button>
|
||||
createButton: function createButton(buttonType, attr) {
|
||||
var button = createElement('button');
|
||||
var attributes = Object.assign({}, attr);
|
||||
var type = toCamelCase(buttonType);
|
||||
var toggle = false;
|
||||
var label;
|
||||
var icon;
|
||||
var labelPressed;
|
||||
var iconPressed;
|
||||
var props = {
|
||||
element: 'button',
|
||||
toggle: false,
|
||||
label: null,
|
||||
icon: null,
|
||||
labelPressed: null,
|
||||
iconPressed: null
|
||||
};
|
||||
['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 (!('type' in attributes)) {
|
||||
if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
|
||||
attributes.type = 'button';
|
||||
}
|
||||
} // Set class name
|
||||
|
||||
if ('class' in attributes) {
|
||||
|
||||
if (Object.keys(attributes).includes('class')) {
|
||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||
attributes.class += " ".concat(this.config.classNames.control);
|
||||
}
|
||||
@ -1392,69 +1424,76 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
switch (buttonType) {
|
||||
case 'play':
|
||||
toggle = true;
|
||||
label = 'play';
|
||||
labelPressed = 'pause';
|
||||
icon = 'play';
|
||||
iconPressed = 'pause';
|
||||
props.toggle = true;
|
||||
props.label = 'play';
|
||||
props.labelPressed = 'pause';
|
||||
props.icon = 'play';
|
||||
props.iconPressed = 'pause';
|
||||
break;
|
||||
|
||||
case 'mute':
|
||||
toggle = true;
|
||||
label = 'mute';
|
||||
labelPressed = 'unmute';
|
||||
icon = 'volume';
|
||||
iconPressed = 'muted';
|
||||
props.toggle = true;
|
||||
props.label = 'mute';
|
||||
props.labelPressed = 'unmute';
|
||||
props.icon = 'volume';
|
||||
props.iconPressed = 'muted';
|
||||
break;
|
||||
|
||||
case 'captions':
|
||||
toggle = true;
|
||||
label = 'enableCaptions';
|
||||
labelPressed = 'disableCaptions';
|
||||
icon = 'captions-off';
|
||||
iconPressed = 'captions-on';
|
||||
props.toggle = true;
|
||||
props.label = 'enableCaptions';
|
||||
props.labelPressed = 'disableCaptions';
|
||||
props.icon = 'captions-off';
|
||||
props.iconPressed = 'captions-on';
|
||||
break;
|
||||
|
||||
case 'fullscreen':
|
||||
toggle = true;
|
||||
label = 'enterFullscreen';
|
||||
labelPressed = 'exitFullscreen';
|
||||
icon = 'enter-fullscreen';
|
||||
iconPressed = 'exit-fullscreen';
|
||||
props.toggle = true;
|
||||
props.label = 'enterFullscreen';
|
||||
props.labelPressed = 'exitFullscreen';
|
||||
props.icon = 'enter-fullscreen';
|
||||
props.iconPressed = 'exit-fullscreen';
|
||||
break;
|
||||
|
||||
case 'play-large':
|
||||
attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
|
||||
type = 'play';
|
||||
label = 'play';
|
||||
icon = 'play';
|
||||
props.label = 'play';
|
||||
props.icon = 'play';
|
||||
break;
|
||||
|
||||
default:
|
||||
label = type;
|
||||
icon = buttonType;
|
||||
} // Setup toggle icon and labels
|
||||
if (is.empty(props.label)) {
|
||||
props.label = type;
|
||||
}
|
||||
|
||||
if (is.empty(props.icon)) {
|
||||
props.icon = buttonType;
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
}
|
||||
|
||||
var button = createElement(props.element); // Setup toggle icon and labels
|
||||
|
||||
if (props.toggle) {
|
||||
// Icon
|
||||
button.appendChild(controls.createIcon.call(this, iconPressed, {
|
||||
button.appendChild(controls.createIcon.call(this, props.iconPressed, {
|
||||
class: 'icon--pressed'
|
||||
}));
|
||||
button.appendChild(controls.createIcon.call(this, icon, {
|
||||
button.appendChild(controls.createIcon.call(this, props.icon, {
|
||||
class: 'icon--not-pressed'
|
||||
})); // Label/Tooltip
|
||||
|
||||
button.appendChild(controls.createLabel.call(this, labelPressed, {
|
||||
button.appendChild(controls.createLabel.call(this, props.labelPressed, {
|
||||
class: 'label--pressed'
|
||||
}));
|
||||
button.appendChild(controls.createLabel.call(this, label, {
|
||||
button.appendChild(controls.createLabel.call(this, props.label, {
|
||||
class: 'label--not-pressed'
|
||||
}));
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
} // Merge attributes
|
||||
button.appendChild(controls.createIcon.call(this, props.icon));
|
||||
button.appendChild(controls.createLabel.call(this, props.label));
|
||||
} // Merge and set attributes
|
||||
|
||||
|
||||
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
||||
@ -2307,6 +2346,17 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
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
|
||||
// TODO: Set order based on order in the config.controls array?
|
||||
create: function create(data) {
|
||||
@ -2512,6 +2562,25 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (this.config.controls.includes('airplay') && support.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
|
||||
|
||||
|
||||
@ -3123,7 +3192,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.4.6/plyr.svg',
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
// Quality default
|
||||
@ -3178,7 +3247,8 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
controls: ['play-large', // 'restart',
|
||||
// 'rewind',
|
||||
'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'],
|
||||
// Localisation
|
||||
i18n: {
|
||||
@ -3198,6 +3268,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
unmute: 'Unmute',
|
||||
enableCaptions: 'Enable captions',
|
||||
disableCaptions: 'Disable captions',
|
||||
download: 'Download',
|
||||
enterFullscreen: 'Enter fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
@ -3226,6 +3297,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
},
|
||||
// URLs
|
||||
urls: {
|
||||
download: null,
|
||||
vimeo: {
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
@ -3250,6 +3322,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
download: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
@ -3262,7 +3335,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
events: [// Events to watch on HTML5 media elements and bubble
|
||||
// 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
|
||||
'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
|
||||
'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
|
||||
'statechange', // Quality
|
||||
'qualitychange', // Ads
|
||||
'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
|
||||
@ -3284,6 +3357,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
fastForward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
download: '[data-plyr="download"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
@ -3382,6 +3456,14 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr states
|
||||
// ==========================================================================
|
||||
var pip = {
|
||||
active: 'picture-in-picture',
|
||||
inactive: 'inline'
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr supported types and providers
|
||||
// ==========================================================================
|
||||
@ -3396,7 +3478,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
};
|
||||
/**
|
||||
* Get provider by URL
|
||||
* @param {string} url
|
||||
* @param {String} url
|
||||
*/
|
||||
|
||||
function getProviderByUrl(url) {
|
||||
@ -3923,8 +4005,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var controls$$1 = this.elements.controls;
|
||||
|
||||
if (controls$$1 && this.config.hideControls) {
|
||||
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover));
|
||||
// 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.)
|
||||
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 +4367,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (!is.element(wrapper)) {
|
||||
return;
|
||||
} // On click play, pause ore restart
|
||||
} // On click play, pause or restart
|
||||
|
||||
|
||||
on.call(player, elements.container, 'click', function (event) {
|
||||
@ -4336,6 +4420,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
on.call(player, player.media, 'qualitychange', function (event) {
|
||||
// Update UI
|
||||
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
|
||||
// Bubble up key events for Edge
|
||||
|
||||
@ -4413,7 +4501,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
this.bind(elements.buttons.captions, 'click', function () {
|
||||
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 () {
|
||||
player.fullscreen.toggle();
|
||||
@ -4476,9 +4568,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
|
||||
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 done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
|
||||
@ -4555,32 +4649,28 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
|
||||
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,
|
||||
elements = player.elements,
|
||||
timers = player.timers;
|
||||
var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
|
||||
timers = player.timers; // 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 () {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0); // Delay a little more for keyboard users
|
||||
setTimeout(function () {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 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 () {
|
||||
return ui.toggleControls.call(player, false);
|
||||
}, delay);
|
||||
}
|
||||
timers.controls = setTimeout(function () {
|
||||
return ui.toggleControls.call(player, false);
|
||||
}, delay);
|
||||
}); // Mouse wheel for volume
|
||||
|
||||
this.bind(elements.inputs.volume, 'wheel', function (event) {
|
||||
@ -5171,6 +5261,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var currentSrc;
|
||||
player.embed.getVideoUrl().then(function (value) {
|
||||
currentSrc = value;
|
||||
controls.setDownloadLink.call(player);
|
||||
}).catch(function (error) {
|
||||
_this2.debug.warn(error);
|
||||
});
|
||||
@ -6724,7 +6815,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (this.config.autoplay) {
|
||||
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
|
||||
// ---------------------------------------
|
||||
@ -7355,17 +7449,26 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}
|
||||
|
||||
var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);
|
||||
var updateStorage = true;
|
||||
|
||||
if (!options.includes(quality)) {
|
||||
var value = closest(options, quality);
|
||||
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
|
||||
quality = value;
|
||||
quality = value; // Don't update storage if quality is not supported
|
||||
|
||||
updateStorage = false;
|
||||
} // Update config
|
||||
|
||||
|
||||
config.selected = quality; // Set quality
|
||||
|
||||
this.media.quality = quality;
|
||||
this.media.quality = quality; // Save to storage
|
||||
|
||||
if (updateStorage) {
|
||||
this.storage.set({
|
||||
quality: quality
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get current quality level
|
||||
@ -7448,6 +7551,16 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
get: function get() {
|
||||
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
|
||||
* @param {input} - the URL for the new poster image
|
||||
@ -7534,19 +7647,27 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}, {
|
||||
key: "pip",
|
||||
set: function set(input) {
|
||||
var states = {
|
||||
pip: 'picture-in-picture',
|
||||
inline: 'inline'
|
||||
}; // Bail if no support
|
||||
|
||||
// Bail if no support
|
||||
if (!support.pip) {
|
||||
return;
|
||||
} // Toggle based on current state if not passed
|
||||
|
||||
|
||||
var toggle = is.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state
|
||||
var toggle = is.boolean(input) ? input : !this.pip; // Toggle based on current state
|
||||
// Safari
|
||||
|
||||
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
|
||||
if (is.function(this.media.webkitSetPresentationMode)) {
|
||||
this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
|
||||
} // Chrome
|
||||
|
||||
|
||||
if (is.function(this.media.requestPictureInPicture)) {
|
||||
if (!this.pip && toggle) {
|
||||
this.media.requestPictureInPicture();
|
||||
} else if (this.pip && !toggle) {
|
||||
document.exitPictureInPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the current picture-in-picture state
|
||||
@ -7555,9 +7676,15 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
get: function get() {
|
||||
if (!support.pip) {
|
||||
return null;
|
||||
}
|
||||
} // Safari
|
||||
|
||||
return this.media.webkitPresentationMode;
|
||||
|
||||
if (!is.empty(this.media.webkitPresentationMode)) {
|
||||
return this.media.webkitPresentationMode === pip.active;
|
||||
} // Chrome
|
||||
|
||||
|
||||
return this.media === document.pictureInPictureElement;
|
||||
}
|
||||
}], [{
|
||||
key: "supported",
|
||||
|
2
dist/plyr.js.map
vendored
2
dist/plyr.min.js
vendored
2
dist/plyr.min.js.map
vendored
319
dist/plyr.polyfilled.js
vendored
@ -2804,6 +2804,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
// Accept a URL object
|
||||
if (instanceOf(input, window.URL)) {
|
||||
return true;
|
||||
} // Must be string from here
|
||||
|
||||
|
||||
if (!isString(input)) {
|
||||
return false;
|
||||
} // Add the protocol if required
|
||||
|
||||
|
||||
@ -3324,9 +3329,25 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
};
|
||||
},
|
||||
// Picture-in-picture support
|
||||
// Safari only currently
|
||||
// Safari & Chrome only currently
|
||||
pip: function () {
|
||||
return !browser.isIPhone && is$1.function(createElement('video').webkitSetPresentationMode);
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
} // Safari
|
||||
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
|
||||
|
||||
|
||||
if (is$1.function(createElement('video').webkitSetPresentationMode)) {
|
||||
return true;
|
||||
} // Chrome
|
||||
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
|
||||
|
||||
|
||||
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}(),
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
@ -3458,10 +3479,6 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input
|
||||
}); // Save to storage
|
||||
|
||||
player.storage.set({
|
||||
quality: input
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -3669,6 +3686,13 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
var resources = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay',
|
||||
html5: 'HTML5',
|
||||
vimeo: 'Vimeo',
|
||||
youtube: 'YouTube'
|
||||
};
|
||||
var i18n = {
|
||||
get: function get() {
|
||||
var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
||||
@ -3681,6 +3705,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var string = getDeep(config.i18n, key);
|
||||
|
||||
if (is$1.empty(string)) {
|
||||
if (Object.keys(resources).includes(key)) {
|
||||
return resources[key];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -3993,23 +4021,18 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if ('href' in use) {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
|
||||
} else {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
|
||||
} // Add <use> to <svg>
|
||||
} // 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>
|
||||
|
||||
icon.appendChild(use);
|
||||
return icon;
|
||||
},
|
||||
// Create hidden text label
|
||||
createLabel: function createLabel(type) {
|
||||
createLabel: function createLabel(key) {
|
||||
var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
// Skip i18n for abbreviations and brand names
|
||||
var universals = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay'
|
||||
};
|
||||
var text = universals[type] || i18n.get(type, this.config);
|
||||
var text = i18n.get(key, this.config);
|
||||
var attributes = Object.assign({}, attr, {
|
||||
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
|
||||
});
|
||||
@ -4031,20 +4054,29 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
},
|
||||
// Create a <button>
|
||||
createButton: function createButton(buttonType, attr) {
|
||||
var button = createElement('button');
|
||||
var attributes = Object.assign({}, attr);
|
||||
var type = toCamelCase(buttonType);
|
||||
var toggle = false;
|
||||
var label;
|
||||
var icon;
|
||||
var labelPressed;
|
||||
var iconPressed;
|
||||
var props = {
|
||||
element: 'button',
|
||||
toggle: false,
|
||||
label: null,
|
||||
icon: null,
|
||||
labelPressed: null,
|
||||
iconPressed: null
|
||||
};
|
||||
['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 (!('type' in attributes)) {
|
||||
if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
|
||||
attributes.type = 'button';
|
||||
}
|
||||
} // Set class name
|
||||
|
||||
if ('class' in attributes) {
|
||||
|
||||
if (Object.keys(attributes).includes('class')) {
|
||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||
attributes.class += " ".concat(this.config.classNames.control);
|
||||
}
|
||||
@ -4055,69 +4087,76 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
switch (buttonType) {
|
||||
case 'play':
|
||||
toggle = true;
|
||||
label = 'play';
|
||||
labelPressed = 'pause';
|
||||
icon = 'play';
|
||||
iconPressed = 'pause';
|
||||
props.toggle = true;
|
||||
props.label = 'play';
|
||||
props.labelPressed = 'pause';
|
||||
props.icon = 'play';
|
||||
props.iconPressed = 'pause';
|
||||
break;
|
||||
|
||||
case 'mute':
|
||||
toggle = true;
|
||||
label = 'mute';
|
||||
labelPressed = 'unmute';
|
||||
icon = 'volume';
|
||||
iconPressed = 'muted';
|
||||
props.toggle = true;
|
||||
props.label = 'mute';
|
||||
props.labelPressed = 'unmute';
|
||||
props.icon = 'volume';
|
||||
props.iconPressed = 'muted';
|
||||
break;
|
||||
|
||||
case 'captions':
|
||||
toggle = true;
|
||||
label = 'enableCaptions';
|
||||
labelPressed = 'disableCaptions';
|
||||
icon = 'captions-off';
|
||||
iconPressed = 'captions-on';
|
||||
props.toggle = true;
|
||||
props.label = 'enableCaptions';
|
||||
props.labelPressed = 'disableCaptions';
|
||||
props.icon = 'captions-off';
|
||||
props.iconPressed = 'captions-on';
|
||||
break;
|
||||
|
||||
case 'fullscreen':
|
||||
toggle = true;
|
||||
label = 'enterFullscreen';
|
||||
labelPressed = 'exitFullscreen';
|
||||
icon = 'enter-fullscreen';
|
||||
iconPressed = 'exit-fullscreen';
|
||||
props.toggle = true;
|
||||
props.label = 'enterFullscreen';
|
||||
props.labelPressed = 'exitFullscreen';
|
||||
props.icon = 'enter-fullscreen';
|
||||
props.iconPressed = 'exit-fullscreen';
|
||||
break;
|
||||
|
||||
case 'play-large':
|
||||
attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
|
||||
type = 'play';
|
||||
label = 'play';
|
||||
icon = 'play';
|
||||
props.label = 'play';
|
||||
props.icon = 'play';
|
||||
break;
|
||||
|
||||
default:
|
||||
label = type;
|
||||
icon = buttonType;
|
||||
} // Setup toggle icon and labels
|
||||
if (is$1.empty(props.label)) {
|
||||
props.label = type;
|
||||
}
|
||||
|
||||
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
|
||||
button.appendChild(controls.createIcon.call(this, iconPressed, {
|
||||
button.appendChild(controls.createIcon.call(this, props.iconPressed, {
|
||||
class: 'icon--pressed'
|
||||
}));
|
||||
button.appendChild(controls.createIcon.call(this, icon, {
|
||||
button.appendChild(controls.createIcon.call(this, props.icon, {
|
||||
class: 'icon--not-pressed'
|
||||
})); // Label/Tooltip
|
||||
|
||||
button.appendChild(controls.createLabel.call(this, labelPressed, {
|
||||
button.appendChild(controls.createLabel.call(this, props.labelPressed, {
|
||||
class: 'label--pressed'
|
||||
}));
|
||||
button.appendChild(controls.createLabel.call(this, label, {
|
||||
button.appendChild(controls.createLabel.call(this, props.label, {
|
||||
class: 'label--not-pressed'
|
||||
}));
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
} // Merge attributes
|
||||
button.appendChild(controls.createIcon.call(this, props.icon));
|
||||
button.appendChild(controls.createLabel.call(this, props.label));
|
||||
} // Merge and set attributes
|
||||
|
||||
|
||||
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
||||
@ -4970,6 +5009,17 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
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
|
||||
// TODO: Set order based on order in the config.controls array?
|
||||
create: function create(data) {
|
||||
@ -5175,6 +5225,25 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (this.config.controls.includes('airplay') && support.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
|
||||
|
||||
|
||||
@ -5786,7 +5855,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.4.6/plyr.svg',
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
// Quality default
|
||||
@ -5841,7 +5910,8 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
controls: ['play-large', // 'restart',
|
||||
// 'rewind',
|
||||
'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'],
|
||||
// Localisation
|
||||
i18n: {
|
||||
@ -5861,6 +5931,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
unmute: 'Unmute',
|
||||
enableCaptions: 'Enable captions',
|
||||
disableCaptions: 'Disable captions',
|
||||
download: 'Download',
|
||||
enterFullscreen: 'Enter fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
@ -5889,6 +5960,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
},
|
||||
// URLs
|
||||
urls: {
|
||||
download: null,
|
||||
vimeo: {
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
@ -5913,6 +5985,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
download: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
@ -5925,7 +5998,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
events: [// Events to watch on HTML5 media elements and bubble
|
||||
// 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
|
||||
'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
|
||||
'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
|
||||
'statechange', // Quality
|
||||
'qualitychange', // Ads
|
||||
'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
|
||||
@ -5947,6 +6020,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
fastForward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
download: '[data-plyr="download"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
@ -6045,6 +6119,14 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr states
|
||||
// ==========================================================================
|
||||
var pip = {
|
||||
active: 'picture-in-picture',
|
||||
inactive: 'inline'
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr supported types and providers
|
||||
// ==========================================================================
|
||||
@ -6059,7 +6141,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
};
|
||||
/**
|
||||
* Get provider by URL
|
||||
* @param {string} url
|
||||
* @param {String} url
|
||||
*/
|
||||
|
||||
function getProviderByUrl(url) {
|
||||
@ -6596,8 +6678,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var controls$$1 = this.elements.controls;
|
||||
|
||||
if (controls$$1 && this.config.hideControls) {
|
||||
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover));
|
||||
// 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.)
|
||||
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 +7040,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (!is$1.element(wrapper)) {
|
||||
return;
|
||||
} // On click play, pause ore restart
|
||||
} // On click play, pause or restart
|
||||
|
||||
|
||||
on.call(player, elements.container, 'click', function (event) {
|
||||
@ -7009,6 +7093,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
on.call(player, player.media, 'qualitychange', function (event) {
|
||||
// Update UI
|
||||
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
|
||||
// Bubble up key events for Edge
|
||||
|
||||
@ -7086,7 +7174,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
this.bind(elements.buttons.captions, 'click', function () {
|
||||
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 () {
|
||||
player.fullscreen.toggle();
|
||||
@ -7149,9 +7241,11 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (is$1.keyboardEvent(event) && code !== 39 && code !== 37) {
|
||||
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 done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
|
||||
@ -7228,32 +7322,28 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
|
||||
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,
|
||||
elements = player.elements,
|
||||
timers = player.timers;
|
||||
var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element
|
||||
timers = player.timers; // 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 () {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0); // Delay a little more for keyboard users
|
||||
setTimeout(function () {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 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 () {
|
||||
return ui.toggleControls.call(player, false);
|
||||
}, delay);
|
||||
}
|
||||
timers.controls = setTimeout(function () {
|
||||
return ui.toggleControls.call(player, false);
|
||||
}, delay);
|
||||
}); // Mouse wheel for volume
|
||||
|
||||
this.bind(elements.inputs.volume, 'wheel', function (event) {
|
||||
@ -7864,6 +7954,7 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
var currentSrc;
|
||||
player.embed.getVideoUrl().then(function (value) {
|
||||
currentSrc = value;
|
||||
controls.setDownloadLink.call(player);
|
||||
}).catch(function (error) {
|
||||
_this2.debug.warn(error);
|
||||
});
|
||||
@ -9414,7 +9505,10 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
|
||||
if (this.config.autoplay) {
|
||||
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
|
||||
// ---------------------------------------
|
||||
@ -10045,17 +10139,26 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}
|
||||
|
||||
var quality = [!is$1.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is$1.number);
|
||||
var updateStorage = true;
|
||||
|
||||
if (!options.includes(quality)) {
|
||||
var value = closest(options, quality);
|
||||
this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
|
||||
quality = value;
|
||||
quality = value; // Don't update storage if quality is not supported
|
||||
|
||||
updateStorage = false;
|
||||
} // Update config
|
||||
|
||||
|
||||
config.selected = quality; // Set quality
|
||||
|
||||
this.media.quality = quality;
|
||||
this.media.quality = quality; // Save to storage
|
||||
|
||||
if (updateStorage) {
|
||||
this.storage.set({
|
||||
quality: quality
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get current quality level
|
||||
@ -10138,6 +10241,16 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
get: function get() {
|
||||
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
|
||||
* @param {input} - the URL for the new poster image
|
||||
@ -10224,19 +10337,27 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
}, {
|
||||
key: "pip",
|
||||
set: function set(input) {
|
||||
var states = {
|
||||
pip: 'picture-in-picture',
|
||||
inline: 'inline'
|
||||
}; // Bail if no support
|
||||
|
||||
// Bail if no support
|
||||
if (!support.pip) {
|
||||
return;
|
||||
} // Toggle based on current state if not passed
|
||||
|
||||
|
||||
var toggle = is$1.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state
|
||||
var toggle = is$1.boolean(input) ? input : !this.pip; // Toggle based on current state
|
||||
// Safari
|
||||
|
||||
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
|
||||
if (is$1.function(this.media.webkitSetPresentationMode)) {
|
||||
this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
|
||||
} // Chrome
|
||||
|
||||
|
||||
if (is$1.function(this.media.requestPictureInPicture)) {
|
||||
if (!this.pip && toggle) {
|
||||
this.media.requestPictureInPicture();
|
||||
} else if (this.pip && !toggle) {
|
||||
document.exitPictureInPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the current picture-in-picture state
|
||||
@ -10245,9 +10366,15 @@ typeof navigator === "object" && (function (global, factory) {
|
||||
get: function get() {
|
||||
if (!support.pip) {
|
||||
return null;
|
||||
}
|
||||
} // Safari
|
||||
|
||||
return this.media.webkitPresentationMode;
|
||||
|
||||
if (!is$1.empty(this.media.webkitPresentationMode)) {
|
||||
return this.media.webkitPresentationMode === pip.active;
|
||||
} // Chrome
|
||||
|
||||
|
||||
return this.media === document.pictureInPictureElement;
|
||||
}
|
||||
}], [{
|
||||
key: "supported",
|
||||
|
2
dist/plyr.polyfilled.js.map
vendored
2
dist/plyr.polyfilled.min.js
vendored
2
dist/plyr.polyfilled.min.js.map
vendored
2
dist/plyr.svg
vendored
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5.3 KiB |
@ -308,13 +308,13 @@ if (Object.keys(credentials).includes('aws') && Object.keys(credentials).include
|
||||
console.log(`Updating versions to '${version}'...`);
|
||||
|
||||
// Replace versioned URLs in source
|
||||
const files = ['plyr.js', 'plyr.polyfilled.js', 'defaults.js'];
|
||||
const files = ['plyr.js', 'plyr.polyfilled.js', 'config/defaults.js'];
|
||||
|
||||
return gulp
|
||||
.src(files.map(file => path.join(root, `src/js/${file}`)))
|
||||
.src(files.map(file => path.join(root, `src/js/${file}`)), { base: '.' })
|
||||
.pipe(replace(semver, `v${version}`))
|
||||
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
|
||||
.pipe(gulp.dest(path.join(root, 'src/js/')));
|
||||
.pipe(gulp.dest('./'));
|
||||
});
|
||||
|
||||
// Publish version to CDN bucket
|
||||
|
16
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.4.4",
|
||||
"version": "3.4.6",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
@ -36,10 +36,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"del": "^3.0.0",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint": "^5.7.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^3.1.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
@ -57,28 +57,28 @@
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-s3": "^0.11.0",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-svgmin": "^2.1.0",
|
||||
"gulp-svgstore": "^7.0.0",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"gulp-util": "^3.0.8",
|
||||
"postcss-custom-properties": "^8.0.5",
|
||||
"postcss-custom-properties": "^8.0.8",
|
||||
"prettier-eslint": "^8.8.2",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^5.0.0",
|
||||
"remark-validate-links": "^7.1.0",
|
||||
"rollup-plugin-babel": "^4.0.3",
|
||||
"rollup-plugin-commonjs": "^9.1.8",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-node-resolve": "^3.4.0",
|
||||
"run-sequence": "^2.2.1",
|
||||
"stylelint": "^9.5.0",
|
||||
"stylelint": "^9.6.0",
|
||||
"stylelint-config-prettier": "^4.0.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.2.0",
|
||||
"stylelint-order": "^1.0.0",
|
||||
"stylelint-scss": "^3.3.1",
|
||||
"stylelint-scss": "^3.3.2",
|
||||
"stylelint-selector-bem-pattern": "^2.0.0",
|
||||
"through2": "^2.0.3"
|
||||
},
|
||||
|
13
readme.md
@ -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.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.4.4/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.4.6/plyr.js"></script>
|
||||
```
|
||||
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.4.4/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.4.6/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.4.4/plyr.css">
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.4.6/plyr.css">
|
||||
```
|
||||
|
||||
### 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
|
||||
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.6/plyr.svg`.
|
||||
|
||||
## Ads
|
||||
|
||||
@ -175,7 +175,7 @@ Any questions regarding the ads can be sent straight to vi.ai and any issues wit
|
||||
### SASS
|
||||
|
||||
You can use `bundle.scss` file included in `/src` as part of your build and change variables to suit your design. The SASS require you to
|
||||
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you be should already!) as all declarations use the W3C definitions.
|
||||
use the [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) plugin (you should be already!) as all declarations use the W3C definitions.
|
||||
|
||||
The HTML markup uses the BEM methodology with `plyr` as the block, e.g. `.plyr__controls`. You can change the class hooks in the options to match any custom CSS
|
||||
you write. Check out the JavaScript source for more on this.
|
||||
@ -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. |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
@ -416,7 +417,7 @@ player.fullscreen.active; // false;
|
||||
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
|
||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
||||
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
|
||||
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
|
||||
|
||||
1. YouTube only. HTML5 will follow.
|
||||
2. HTML5 only
|
||||
|
@ -60,7 +60,7 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.4.6/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
@ -133,6 +133,7 @@ const defaults = {
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
// 'download',
|
||||
'fullscreen',
|
||||
],
|
||||
settings: ['captions', 'quality', 'speed'],
|
||||
@ -155,6 +156,7 @@ const defaults = {
|
||||
unmute: 'Unmute',
|
||||
enableCaptions: 'Enable captions',
|
||||
disableCaptions: 'Disable captions',
|
||||
download: 'Download',
|
||||
enterFullscreen: 'Enter fullscreen',
|
||||
exitFullscreen: 'Exit fullscreen',
|
||||
frameTitle: 'Player for {title}',
|
||||
@ -184,6 +186,7 @@ const defaults = {
|
||||
|
||||
// URLs
|
||||
urls: {
|
||||
download: null,
|
||||
vimeo: {
|
||||
sdk: 'https://player.vimeo.com/api/player.js',
|
||||
iframe: 'https://player.vimeo.com/video/{0}?{1}',
|
||||
@ -210,6 +213,7 @@ const defaults = {
|
||||
mute: null,
|
||||
volume: null,
|
||||
captions: null,
|
||||
download: null,
|
||||
fullscreen: null,
|
||||
pip: null,
|
||||
airplay: null,
|
||||
@ -245,6 +249,7 @@ const defaults = {
|
||||
'cuechange',
|
||||
|
||||
// Custom events
|
||||
'download',
|
||||
'enterfullscreen',
|
||||
'exitfullscreen',
|
||||
'captionsenabled',
|
||||
@ -290,6 +295,7 @@ const defaults = {
|
||||
fastForward: '[data-plyr="fast-forward"]',
|
||||
mute: '[data-plyr="mute"]',
|
||||
captions: '[data-plyr="captions"]',
|
||||
download: '[data-plyr="download"]',
|
||||
fullscreen: '[data-plyr="fullscreen"]',
|
||||
pip: '[data-plyr="pip"]',
|
||||
airplay: '[data-plyr="airplay"]',
|
||||
|
10
src/js/config/states.js
Normal file
@ -0,0 +1,10 @@
|
||||
// ==========================================================================
|
||||
// Plyr states
|
||||
// ==========================================================================
|
||||
|
||||
export const pip = {
|
||||
active: 'picture-in-picture',
|
||||
inactive: 'inline',
|
||||
};
|
||||
|
||||
export default { pip };
|
@ -15,7 +15,7 @@ export const types = {
|
||||
|
||||
/**
|
||||
* Get provider by URL
|
||||
* @param {string} url
|
||||
* @param {String} url
|
||||
*/
|
||||
export function getProviderByUrl(url) {
|
||||
// YouTube
|
||||
|
146
src/js/controls.js
vendored
@ -111,10 +111,11 @@ const controls = {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
|
||||
if ('href' in use) {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
|
||||
} else {
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
|
||||
}
|
||||
|
||||
// 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>
|
||||
icon.appendChild(use);
|
||||
|
||||
@ -122,17 +123,13 @@ const controls = {
|
||||
},
|
||||
|
||||
// Create hidden text label
|
||||
createLabel(type, attr = {}) {
|
||||
// Skip i18n for abbreviations and brand names
|
||||
const universals = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay',
|
||||
};
|
||||
const text = universals[type] || i18n.get(type, this.config);
|
||||
createLabel(key, attr = {}) {
|
||||
const text = i18n.get(key, this.config);
|
||||
|
||||
const attributes = Object.assign({}, attr, {
|
||||
class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' '),
|
||||
});
|
||||
|
||||
return createElement('span', attributes, text);
|
||||
},
|
||||
|
||||
@ -161,21 +158,32 @@ const controls = {
|
||||
|
||||
// Create a <button>
|
||||
createButton(buttonType, attr) {
|
||||
const button = createElement('button');
|
||||
const attributes = Object.assign({}, attr);
|
||||
let type = toCamelCase(buttonType);
|
||||
|
||||
let toggle = false;
|
||||
let label;
|
||||
let icon;
|
||||
let labelPressed;
|
||||
let iconPressed;
|
||||
const props = {
|
||||
element: 'button',
|
||||
toggle: false,
|
||||
label: null,
|
||||
icon: null,
|
||||
labelPressed: null,
|
||||
iconPressed: null,
|
||||
};
|
||||
|
||||
if (!('type' in attributes)) {
|
||||
['element', 'icon', 'label'].forEach(key => {
|
||||
if (Object.keys(attributes).includes(key)) {
|
||||
props[key] = attributes[key];
|
||||
delete attributes[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Default to 'button' type to prevent form submission
|
||||
if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
|
||||
attributes.type = 'button';
|
||||
}
|
||||
|
||||
if ('class' in attributes) {
|
||||
// Set class name
|
||||
if (Object.keys(attributes).includes('class')) {
|
||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||
attributes.class += ` ${this.config.classNames.control}`;
|
||||
}
|
||||
@ -186,82 +194,87 @@ const controls = {
|
||||
// Large play button
|
||||
switch (buttonType) {
|
||||
case 'play':
|
||||
toggle = true;
|
||||
label = 'play';
|
||||
labelPressed = 'pause';
|
||||
icon = 'play';
|
||||
iconPressed = 'pause';
|
||||
props.toggle = true;
|
||||
props.label = 'play';
|
||||
props.labelPressed = 'pause';
|
||||
props.icon = 'play';
|
||||
props.iconPressed = 'pause';
|
||||
break;
|
||||
|
||||
case 'mute':
|
||||
toggle = true;
|
||||
label = 'mute';
|
||||
labelPressed = 'unmute';
|
||||
icon = 'volume';
|
||||
iconPressed = 'muted';
|
||||
props.toggle = true;
|
||||
props.label = 'mute';
|
||||
props.labelPressed = 'unmute';
|
||||
props.icon = 'volume';
|
||||
props.iconPressed = 'muted';
|
||||
break;
|
||||
|
||||
case 'captions':
|
||||
toggle = true;
|
||||
label = 'enableCaptions';
|
||||
labelPressed = 'disableCaptions';
|
||||
icon = 'captions-off';
|
||||
iconPressed = 'captions-on';
|
||||
props.toggle = true;
|
||||
props.label = 'enableCaptions';
|
||||
props.labelPressed = 'disableCaptions';
|
||||
props.icon = 'captions-off';
|
||||
props.iconPressed = 'captions-on';
|
||||
break;
|
||||
|
||||
case 'fullscreen':
|
||||
toggle = true;
|
||||
label = 'enterFullscreen';
|
||||
labelPressed = 'exitFullscreen';
|
||||
icon = 'enter-fullscreen';
|
||||
iconPressed = 'exit-fullscreen';
|
||||
props.toggle = true;
|
||||
props.label = 'enterFullscreen';
|
||||
props.labelPressed = 'exitFullscreen';
|
||||
props.icon = 'enter-fullscreen';
|
||||
props.iconPressed = 'exit-fullscreen';
|
||||
break;
|
||||
|
||||
case 'play-large':
|
||||
attributes.class += ` ${this.config.classNames.control}--overlaid`;
|
||||
type = 'play';
|
||||
label = 'play';
|
||||
icon = 'play';
|
||||
props.label = 'play';
|
||||
props.icon = 'play';
|
||||
break;
|
||||
|
||||
default:
|
||||
label = type;
|
||||
icon = buttonType;
|
||||
if (is.empty(props.label)) {
|
||||
props.label = type;
|
||||
}
|
||||
if (is.empty(props.icon)) {
|
||||
props.icon = buttonType;
|
||||
}
|
||||
}
|
||||
|
||||
const button = createElement(props.element);
|
||||
|
||||
// Setup toggle icon and labels
|
||||
if (toggle) {
|
||||
if (props.toggle) {
|
||||
// Icon
|
||||
button.appendChild(
|
||||
controls.createIcon.call(this, iconPressed, {
|
||||
controls.createIcon.call(this, props.iconPressed, {
|
||||
class: 'icon--pressed',
|
||||
}),
|
||||
);
|
||||
button.appendChild(
|
||||
controls.createIcon.call(this, icon, {
|
||||
controls.createIcon.call(this, props.icon, {
|
||||
class: 'icon--not-pressed',
|
||||
}),
|
||||
);
|
||||
|
||||
// Label/Tooltip
|
||||
button.appendChild(
|
||||
controls.createLabel.call(this, labelPressed, {
|
||||
controls.createLabel.call(this, props.labelPressed, {
|
||||
class: 'label--pressed',
|
||||
}),
|
||||
);
|
||||
button.appendChild(
|
||||
controls.createLabel.call(this, label, {
|
||||
controls.createLabel.call(this, props.label, {
|
||||
class: 'label--not-pressed',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
button.appendChild(controls.createIcon.call(this, props.icon));
|
||||
button.appendChild(controls.createLabel.call(this, props.label));
|
||||
}
|
||||
|
||||
// Merge attributes
|
||||
// Merge and set attributes
|
||||
extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
|
||||
|
||||
setAttributes(button, attributes);
|
||||
|
||||
// We have multiple play buttons
|
||||
@ -1214,6 +1227,19 @@ const controls = {
|
||||
controls.focusFirstMenuItem.call(this, target, tabFocus);
|
||||
},
|
||||
|
||||
// Set the download link
|
||||
setDownloadLink() {
|
||||
const 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
|
||||
// TODO: Set order based on order in the config.controls array?
|
||||
create(data) {
|
||||
@ -1490,6 +1516,26 @@ const controls = {
|
||||
container.appendChild(controls.createButton.call(this, 'airplay'));
|
||||
}
|
||||
|
||||
// Download button
|
||||
if (this.config.controls.includes('download')) {
|
||||
const attributes = {
|
||||
element: 'a',
|
||||
href: this.download,
|
||||
target: '_blank',
|
||||
};
|
||||
|
||||
const { download } = this.config.urls;
|
||||
|
||||
if (!is.url(download) && this.isEmbed) {
|
||||
extend(attributes, {
|
||||
icon: `logo-${this.provider}`,
|
||||
label: this.provider,
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(controls.createButton.call(this, 'download', attributes));
|
||||
}
|
||||
|
||||
// Toggle fullscreen button
|
||||
if (this.config.controls.includes('fullscreen')) {
|
||||
container.appendChild(controls.createButton.call(this, 'fullscreen'));
|
||||
|
@ -82,9 +82,6 @@ const html5 = {
|
||||
triggerEvent.call(player, player.media, 'qualitychange', false, {
|
||||
quality: input,
|
||||
});
|
||||
|
||||
// Save to storage
|
||||
player.storage.set({ quality: input });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -371,7 +371,7 @@ class Listeners {
|
||||
return;
|
||||
}
|
||||
|
||||
// On click play, pause ore restart
|
||||
// On click play, pause or restart
|
||||
on.call(player, elements.container, 'click', event => {
|
||||
const targets = [elements.container, wrapper];
|
||||
|
||||
@ -431,6 +431,11 @@ class Listeners {
|
||||
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', () => {
|
||||
controls.setDownloadLink.call(player);
|
||||
});
|
||||
|
||||
// Proxy events to container
|
||||
// Bubble up key events for Edge
|
||||
const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
|
||||
@ -517,6 +522,16 @@ class Listeners {
|
||||
// Captions toggle
|
||||
this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
|
||||
|
||||
// Download
|
||||
this.bind(
|
||||
elements.buttons.download,
|
||||
'click',
|
||||
() => {
|
||||
triggerEvent.call(player, player.media, 'download');
|
||||
},
|
||||
'download',
|
||||
);
|
||||
|
||||
// Fullscreen toggle
|
||||
this.bind(
|
||||
elements.buttons.fullscreen,
|
||||
@ -605,6 +620,9 @@ class Listeners {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record seek time so we can prevent hiding controls for a few seconds after seek
|
||||
player.lastSeekTime = Date.now();
|
||||
|
||||
// Was playing before?
|
||||
const play = seek.hasAttribute(attribute);
|
||||
|
||||
@ -697,33 +715,29 @@ class Listeners {
|
||||
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||
});
|
||||
|
||||
// Focus in/out on controls
|
||||
this.bind(elements.controls, 'focusin focusout', event => {
|
||||
// Show controls when they receive focus (e.g., when using keyboard tab key)
|
||||
this.bind(elements.controls, 'focusin', () => {
|
||||
const { config, elements, timers } = player;
|
||||
const isFocusIn = event.type === 'focusin';
|
||||
|
||||
// Skip transition to prevent focus from scrolling the parent element
|
||||
toggleClass(elements.controls, config.classNames.noTransition, isFocusIn);
|
||||
toggleClass(elements.controls, config.classNames.noTransition, true);
|
||||
|
||||
// Toggle
|
||||
ui.toggleControls.call(player, isFocusIn);
|
||||
ui.toggleControls.call(player, true);
|
||||
|
||||
// If focusin, hide again after delay
|
||||
if (isFocusIn) {
|
||||
// Restore transition
|
||||
setTimeout(() => {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0);
|
||||
// Restore transition
|
||||
setTimeout(() => {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0);
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
const delay = this.touch ? 3000 : 4000;
|
||||
// Delay a little more for mouse users
|
||||
const delay = this.touch ? 3000 : 4000;
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(timers.controls);
|
||||
// Clear timer
|
||||
clearTimeout(timers.controls);
|
||||
|
||||
// Hide
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
|
||||
}
|
||||
// Hide again after delay
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
|
||||
});
|
||||
|
||||
// Mouse wheel for volume
|
||||
|
@ -71,7 +71,7 @@ const vimeo = {
|
||||
// For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
|
||||
setAspectRatio(input) {
|
||||
const [x, y] = (is.string(input) ? input : this.config.ratio).split(':');
|
||||
const padding = 100 / x * y;
|
||||
const padding = (100 / x) * y;
|
||||
this.elements.wrapper.style.paddingBottom = `${padding}%`;
|
||||
|
||||
if (this.supported.ui) {
|
||||
@ -278,6 +278,7 @@ const vimeo = {
|
||||
.getVideoUrl()
|
||||
.then(value => {
|
||||
currentSrc = value;
|
||||
controls.setDownloadLink.call(player);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn(error);
|
||||
|
@ -1,12 +1,13 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.4.4
|
||||
// plyr.js v3.4.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import captions from './captions';
|
||||
import defaults from './config/defaults';
|
||||
import { pip } from './config/states';
|
||||
import { getProviderByUrl, providers, types } from './config/types';
|
||||
import Console from './console';
|
||||
import controls from './controls';
|
||||
@ -302,6 +303,9 @@ class Plyr {
|
||||
if (this.config.autoplay) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
@ -692,10 +696,15 @@ class Plyr {
|
||||
config.default,
|
||||
].find(is.number);
|
||||
|
||||
let updateStorage = true;
|
||||
|
||||
if (!options.includes(quality)) {
|
||||
const value = closest(options, quality);
|
||||
this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
|
||||
quality = value;
|
||||
|
||||
// Don't update storage if quality is not supported
|
||||
updateStorage = false;
|
||||
}
|
||||
|
||||
// Update config
|
||||
@ -703,6 +712,11 @@ class Plyr {
|
||||
|
||||
// Set quality
|
||||
this.media.quality = quality;
|
||||
|
||||
// Save to storage
|
||||
if (updateStorage) {
|
||||
this.storage.set({ quality });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -788,6 +802,15 @@ class Plyr {
|
||||
return this.media.currentSrc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a download URL (either source or custom)
|
||||
*/
|
||||
get download() {
|
||||
const { download } = this.config.urls;
|
||||
|
||||
return is.url(download) ? download : this.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the poster image for a video
|
||||
* @param {input} - the URL for the new poster image
|
||||
@ -874,21 +897,28 @@ class Plyr {
|
||||
* TODO: detect outside changes
|
||||
*/
|
||||
set pip(input) {
|
||||
const states = {
|
||||
pip: 'picture-in-picture',
|
||||
inline: 'inline',
|
||||
};
|
||||
|
||||
// Bail if no support
|
||||
if (!support.pip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle based on current state if not passed
|
||||
const toggle = is.boolean(input) ? input : this.pip === states.inline;
|
||||
const toggle = is.boolean(input) ? input : !this.pip;
|
||||
|
||||
// Toggle based on current state
|
||||
this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
|
||||
// Safari
|
||||
if (is.function(this.media.webkitSetPresentationMode)) {
|
||||
this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
|
||||
}
|
||||
|
||||
// Chrome
|
||||
if (is.function(this.media.requestPictureInPicture)) {
|
||||
if (!this.pip && toggle) {
|
||||
this.media.requestPictureInPicture();
|
||||
} else if (this.pip && !toggle) {
|
||||
document.exitPictureInPicture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -899,7 +929,13 @@ class Plyr {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.media.webkitPresentationMode;
|
||||
// Safari
|
||||
if (!is.empty(this.media.webkitPresentationMode)) {
|
||||
return this.media.webkitPresentationMode === pip.active;
|
||||
}
|
||||
|
||||
// Chrome
|
||||
return this.media === document.pictureInPictureElement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.4.4
|
||||
// plyr.js v3.4.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
@ -36,8 +36,26 @@ const support = {
|
||||
},
|
||||
|
||||
// Picture-in-picture support
|
||||
// Safari only currently
|
||||
pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(),
|
||||
// Safari & Chrome only currently
|
||||
pip: (() => {
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Safari
|
||||
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
|
||||
if (is.function(createElement('video').webkitSetPresentationMode)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Chrome
|
||||
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
|
||||
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})(),
|
||||
|
||||
// Airplay support
|
||||
// Safari only currently
|
||||
|
@ -247,8 +247,11 @@ const ui = {
|
||||
const { controls } = this.elements;
|
||||
|
||||
if (controls && this.config.hideControls) {
|
||||
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover));
|
||||
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
||||
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
|
||||
|
||||
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -6,6 +6,15 @@ import is from './is';
|
||||
import { getDeep } from './objects';
|
||||
import { replaceAll } from './strings';
|
||||
|
||||
// Skip i18n for abbreviations and brand names
|
||||
const resources = {
|
||||
pip: 'PIP',
|
||||
airplay: 'AirPlay',
|
||||
html5: 'HTML5',
|
||||
vimeo: 'Vimeo',
|
||||
youtube: 'YouTube',
|
||||
};
|
||||
|
||||
const i18n = {
|
||||
get(key = '', config = {}) {
|
||||
if (is.empty(key) || is.empty(config)) {
|
||||
@ -15,6 +24,10 @@ const i18n = {
|
||||
let string = getDeep(config.i18n, key);
|
||||
|
||||
if (is.empty(string)) {
|
||||
if (Object.keys(resources).includes(key)) {
|
||||
return resources[key];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,11 @@ const isUrl = input => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must be string from here
|
||||
if (!isString(input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the protocol if required
|
||||
let string = input;
|
||||
if (!input.startsWith('http://') || !input.startsWith('https://')) {
|
||||
|
@ -33,6 +33,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any link styling
|
||||
a.plyr__control {
|
||||
text-decoration: none;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Change icons on state change
|
||||
.plyr__control:not(.plyr__control--pressed) .icon--pressed,
|
||||
.plyr__control.plyr__control--pressed .icon--not-pressed,
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M16,1 L2,1 C1.447,1 1,1.447 1,2 L1,12 C1,12.553 1.447,13 2,13 L5,13 L5,11 L3,11 L3,3 L15,3 L15,11 L13,11 L13,13 L16,13 C16.553,13 17,12.553 17,12 L17,2 C17,1.447 16.553,1 16,1 L16,1 Z"></path>
|
||||
<polygon points="4 17 14 17 9 11"></polygon>
|
||||
|
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 374 B |
@ -1,6 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill-rule="evenodd" fill-opacity="0.5">
|
||||
<path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path>
|
||||
</g>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 945 B |
@ -1,6 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill-rule="evenodd">
|
||||
<path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path>
|
||||
</g>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 926 B |
6
src/sprite/plyr-download.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(2 1)">
|
||||
<path d="M7,12 C7.3,12 7.5,11.9 7.7,11.7 L13.4,6 L12,4.6 L8,8.6 L8,0 L6,0 L6,8.6 L2,4.6 L0.6,6 L6.3,11.7 C6.5,11.9 6.7,12 7,12 Z" />
|
||||
<rect width="14" height="2" y="14" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 325 B |
@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon>
|
||||
<polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon>
|
||||
<polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>
|
||||
</svg>
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 264 B |
@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="1 12 4.6 12 0.6 16 2 17.4 6 13.4 6 17 8 17 8 10 1 10"></polygon>
|
||||
<polygon points="16 0.6 12 4.6 12 1 10 1 10 8 17 8 17 6 13.4 6 17.4 2"></polygon>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="1 12 4.6 12 0.6 16 2 17.4 6 13.4 6 17 8 17 8 10 1 10"></polygon>
|
||||
<polygon points="16 0.6 12 4.6 12 1 10 1 10 8 17 8 17 6 13.4 6 17.4 2"></polygon>
|
||||
</svg>
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 266 B |
@ -1,6 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="7.875 7.17142857 0 1 0 17 7.875 10.8285714 7.875 17 18 9 7.875 1"></polygon>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="7.875 7.17142857 0 1 0 17 7.875 10.8285714 7.875 17 18 9 7.875 1"></polygon>
|
||||
</svg>
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 192 B |
4
src/sprite/plyr-logo-vimeo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<path d="M16,3.3 C15.9,4.9 14.8,7 12.7,9.7 C10.5,12.5 8.7,13.9 7.2,13.9 C6.3,13.9 5.5,13 4.8,11.3 C4,8.9 3.4,4 2,4 C1.9,4 1.5,4.3 0.8,4.8 L0,3.8 C0.8,3.1 3.5,0.4 4.7,0.3 C5.9,0.2 6.7,1 7,2.8 C7.3,4.8 7.8,8.9 8.8,8.9 C9.7,8.9 11.3,5.5 11.4,4.9 C11.5,4 11.1,3 9.1,3.8 C9.9,1.2 11.4,-8.8817842e-16 13.6,-8.8817842e-16 C15.3,0.1 16.1,1.2 16,3.3 Z"
|
||||
transform="translate(1 2)" />
|
||||
</svg>
|
After Width: | Height: | Size: 470 B |
4
src/sprite/plyr-logo-youtube.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<path d="M15.8,2.8 C15.6,1.5 15,0.6 13.6,0.4 C11.4,0 8,0 8,0 C8,0 4.6,0 2.4,0.4 C1,0.6 0.3,1.5 0.2,2.8 C0,4.1 0,6 0,6 C0,6 0,7.9 0.2,9.2 C0.4,10.5 1,11.4 2.4,11.6 C4.6,12 8,12 8,12 C8,12 11.4,12 13.6,11.6 C15,11.3 15.6,10.5 15.8,9.2 C16,7.9 16,6 16,6 C16,6 16,4.1 15.8,2.8 Z M6,9 L6,3 L11,6 L6,9 Z"
|
||||
transform="translate(1 3)" />
|
||||
</svg>
|
After Width: | Height: | Size: 425 B |
@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="12.4 12.5 14.5 10.4 16.6 12.5 18 11.1 15.9 9 18 6.9 16.6 5.5 14.5 7.6 12.4 5.5 11 6.9 13.1 9 11 11.1"></polygon>
|
||||
<path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="12.4 12.5 14.5 10.4 16.6 12.5 18 11.1 15.9 9 18 6.9 16.6 5.5 14.5 7.6 12.4 5.5 11 6.9 13.1 9 11 11.1"></polygon>
|
||||
<path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 613 B |
@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M6,1 L3,1 C2.4,1 2,1.4 2,2 L2,16 C2,16.6 2.4,17 3,17 L6,17 C6.6,17 7,16.6 7,16 L7,2 C7,1.4 6.6,1 6,1 L6,1 Z"></path>
|
||||
<path d="M12,1 C11.4,1 11,1.4 11,2 L11,16 C11,16.6 11.4,17 12,17 L15,17 C15.6,17 16,16.6 16,16 L16,2 C16,1.4 15.6,1 15,1 L12,1 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6,1 L3,1 C2.4,1 2,1.4 2,2 L2,16 C2,16.6 2.4,17 3,17 L6,17 C6.6,17 7,16.6 7,16 L7,2 C7,1.4 6.6,1 6,1 L6,1 Z"></path>
|
||||
<path d="M12,1 C11.4,1 11,1.4 11,2 L11,16 C11,16.6 11.4,17 12,17 L15,17 C15.6,17 16,16.6 16,16 L16,2 C16,1.4 15.6,1 15,1 L12,1 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 366 B |
@ -1,7 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="13.293 3.293 7.022 9.564 8.436 10.978 14.707 4.707 17 7 17 1 11 1"></polygon>
|
||||
<path d="M13,15 L3,15 L3,5 L8,5 L8,3 L2,3 C1.448,3 1,3.448 1,4 L1,16 C1,16.552 1.448,17 2,17 L14,17 C14.552,17 15,16.552 15,16 L15,10 L13,10 L13,15 L13,15 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="13.293 3.293 7.022 9.564 8.436 10.978 14.707 4.707 17 7 17 1 11 1"></polygon>
|
||||
<path d="M13,15 L3,15 L3,5 L8,5 L8,3 L2,3 C1.448,3 1,3.448 1,4 L1,16 C1,16.552 1.448,17 2,17 L14,17 C14.552,17 15,16.552 15,16 L15,10 L13,10 L13,15 L13,15 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 363 B |
@ -1,6 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M15.5615866,8.10002147 L3.87056367,0.225209313 C3.05219207,-0.33727727 2,0.225209313 2,1.12518784 L2,16.8748122 C2,17.7747907 3.05219207,18.3372773 3.87056367,17.7747907 L15.5615866,9.89997853 C16.1461378,9.44998927 16.1461378,8.55001073 15.5615866,8.10002147 L15.5615866,8.10002147 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.5615866,8.10002147 L3.87056367,0.225209313 C3.05219207,-0.33727727 2,0.225209313 2,1.12518784 L2,16.8748122 C2,17.7747907 3.05219207,18.3372773 3.87056367,17.7747907 L15.5615866,9.89997853 C16.1461378,9.44998927 16.1461378,8.55001073 15.5615866,8.10002147 L15.5615866,8.10002147 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 401 B |
@ -1,6 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M9.7,1.2 L10.4,7.6 L12.5,5.5 C14.4,7.4 14.4,10.6 12.5,12.5 C11.6,13.5 10.3,14 9,14 C7.7,14 6.4,13.5 5.5,12.5 C3.6,10.6 3.6,7.4 5.5,5.5 C6.1,4.9 6.9,4.4 7.8,4.2 L7.2,2.3 C6,2.6 4.9,3.2 4,4.1 C1.3,6.8 1.3,11.2 4,14 C5.3,15.3 7.1,16 8.9,16 C10.8,16 12.5,15.3 13.8,14 C16.5,11.3 16.5,6.9 13.8,4.1 L16,1.9 L9.7,1.2 L9.7,1.2 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.7,1.2 L10.4,7.6 L12.5,5.5 C14.4,7.4 14.4,10.6 12.5,12.5 C11.6,13.5 10.3,14 9,14 C7.7,14 6.4,13.5 5.5,12.5 C3.6,10.6 3.6,7.4 5.5,5.5 C6.1,4.9 6.9,4.4 7.8,4.2 L7.2,2.3 C6,2.6 4.9,3.2 4,4.1 C1.3,6.8 1.3,11.2 4,14 C5.3,15.3 7.1,16 8.9,16 C10.8,16 12.5,15.3 13.8,14 C16.5,11.3 16.5,6.9 13.8,4.1 L16,1.9 L9.7,1.2 L9.7,1.2 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 570 B After Width: | Height: | Size: 437 B |
@ -1,6 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<polygon points="10.125 1 0 9 10.125 17 10.125 10.8285714 18 17 18 1 10.125 7.17142857"></polygon>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="10.125 1 0 9 10.125 17 10.125 10.8285714 18 17 18 1 10.125 7.17142857"></polygon>
|
||||
</svg>
|
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 197 B |
@ -1,6 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M16.135,7.784 C14.832,7.458 14.214,5.966 14.905,4.815 C15.227,4.279 15.13,3.817 14.811,3.499 L14.501,3.189 C14.183,2.871 13.721,2.774 13.185,3.095 C12.033,3.786 10.541,3.168 10.216,1.865 C10.065,1.258 9.669,1 9.219,1 L8.781,1 C8.331,1 7.936,1.258 7.784,1.865 C7.458,3.168 5.966,3.786 4.815,3.095 C4.279,2.773 3.816,2.87 3.498,3.188 L3.188,3.498 C2.87,3.816 2.773,4.279 3.095,4.815 C3.786,5.967 3.168,7.459 1.865,7.784 C1.26,7.935 1,8.33 1,8.781 L1,9.219 C1,9.669 1.258,10.064 1.865,10.216 C3.168,10.542 3.786,12.034 3.095,13.185 C2.773,13.721 2.87,14.183 3.189,14.501 L3.499,14.811 C3.818,15.13 4.281,15.226 4.815,14.905 C5.967,14.214 7.459,14.832 7.784,16.135 C7.935,16.742 8.331,17 8.781,17 L9.219,17 C9.669,17 10.064,16.742 10.216,16.135 C10.542,14.832 12.034,14.214 13.185,14.905 C13.72,15.226 14.182,15.13 14.501,14.811 L14.811,14.501 C15.129,14.183 15.226,13.72 14.905,13.185 C14.214,12.033 14.832,10.541 16.135,10.216 C16.742,10.065 17,9.669 17,9.219 L17,8.781 C17,8.33 16.74,7.935 16.135,7.784 L16.135,7.784 Z M9,12 C7.343,12 6,10.657 6,9 C6,7.343 7.343,6 9,6 C10.657,6 12,7.343 12,9 C12,10.657 10.657,12 9,12 L9,12 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.135,7.784 C14.832,7.458 14.214,5.966 14.905,4.815 C15.227,4.279 15.13,3.817 14.811,3.499 L14.501,3.189 C14.183,2.871 13.721,2.774 13.185,3.095 C12.033,3.786 10.541,3.168 10.216,1.865 C10.065,1.258 9.669,1 9.219,1 L8.781,1 C8.331,1 7.936,1.258 7.784,1.865 C7.458,3.168 5.966,3.786 4.815,3.095 C4.279,2.773 3.816,2.87 3.498,3.188 L3.188,3.498 C2.87,3.816 2.773,4.279 3.095,4.815 C3.786,5.967 3.168,7.459 1.865,7.784 C1.26,7.935 1,8.33 1,8.781 L1,9.219 C1,9.669 1.258,10.064 1.865,10.216 C3.168,10.542 3.786,12.034 3.095,13.185 C2.773,13.721 2.87,14.183 3.189,14.501 L3.499,14.811 C3.818,15.13 4.281,15.226 4.815,14.905 C5.967,14.214 7.459,14.832 7.784,16.135 C7.935,16.742 8.331,17 8.781,17 L9.219,17 C9.669,17 10.064,16.742 10.216,16.135 C10.542,14.832 12.034,14.214 13.185,14.905 C13.72,15.226 14.182,15.13 14.501,14.811 L14.811,14.501 C15.129,14.183 15.226,13.72 14.905,13.185 C14.214,12.033 14.832,10.541 16.135,10.216 C16.742,10.065 17,9.669 17,9.219 L17,8.781 C17,8.33 16.74,7.935 16.135,7.784 L16.135,7.784 Z M9,12 C7.343,12 6,10.657 6,9 C6,7.343 7.343,6 9,6 C10.657,6 12,7.343 12,9 C12,10.657 10.657,12 9,12 L9,12 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@ -1,8 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M15.5999996,3.3 C15.1999996,2.9 14.5999996,2.9 14.1999996,3.3 C13.7999996,3.7 13.7999996,4.3 14.1999996,4.7 C15.3999996,5.9 15.9999996,7.4 15.9999996,9 C15.9999996,10.6 15.3999996,12.1 14.1999996,13.3 C13.7999996,13.7 13.7999996,14.3 14.1999996,14.7 C14.3999996,14.9 14.6999996,15 14.8999996,15 C15.1999996,15 15.3999996,14.9 15.5999996,14.7 C17.0999996,13.2 17.9999996,11.2 17.9999996,9 C17.9999996,6.8 17.0999996,4.8 15.5999996,3.3 L15.5999996,3.3 Z"></path>
|
||||
<path d="M11.2819745,5.28197449 C10.9060085,5.65794047 10.9060085,6.22188944 11.2819745,6.59785542 C12.0171538,7.33303477 12.2772954,8.05605449 12.2772954,9.00000021 C12.2772954,9.93588462 11.851678,10.9172014 11.2819745,11.4869049 C10.9060085,11.8628709 10.9060085,12.4268199 11.2819745,12.8027859 C11.4271642,12.9479755 11.9176724,13.0649528 12.2998149,12.9592565 C12.4124479,12.9281035 12.5156669,12.8776063 12.5978555,12.8027859 C13.773371,11.732654 14.1311161,10.1597914 14.1312523,9.00000021 C14.1312723,8.8299555 14.1286311,8.66015647 14.119665,8.4897429 C14.0674781,7.49784946 13.8010171,6.48513613 12.5978554,5.28197449 C12.2218894,4.9060085 11.6579405,4.9060085 11.2819745,5.28197449 Z"></path>
|
||||
<path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>
|
||||
</g>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.5999996,3.3 C15.1999996,2.9 14.5999996,2.9 14.1999996,3.3 C13.7999996,3.7 13.7999996,4.3 14.1999996,4.7 C15.3999996,5.9 15.9999996,7.4 15.9999996,9 C15.9999996,10.6 15.3999996,12.1 14.1999996,13.3 C13.7999996,13.7 13.7999996,14.3 14.1999996,14.7 C14.3999996,14.9 14.6999996,15 14.8999996,15 C15.1999996,15 15.3999996,14.9 15.5999996,14.7 C17.0999996,13.2 17.9999996,11.2 17.9999996,9 C17.9999996,6.8 17.0999996,4.8 15.5999996,3.3 L15.5999996,3.3 Z"></path>
|
||||
<path d="M11.2819745,5.28197449 C10.9060085,5.65794047 10.9060085,6.22188944 11.2819745,6.59785542 C12.0171538,7.33303477 12.2772954,8.05605449 12.2772954,9.00000021 C12.2772954,9.93588462 11.851678,10.9172014 11.2819745,11.4869049 C10.9060085,11.8628709 10.9060085,12.4268199 11.2819745,12.8027859 C11.4271642,12.9479755 11.9176724,13.0649528 12.2998149,12.9592565 C12.4124479,12.9281035 12.5156669,12.8776063 12.5978555,12.8027859 C13.773371,11.732654 14.1311161,10.1597914 14.1312523,9.00000021 C14.1312723,8.8299555 14.1286311,8.66015647 14.119665,8.4897429 C14.0674781,7.49784946 13.8010171,6.48513613 12.5978554,5.28197449 C12.2218894,4.9060085 11.6579405,4.9060085 11.2819745,5.28197449 Z"></path>
|
||||
<path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |