aboutsummaryrefslogtreecommitdiffstats
path: root/dist/plyr.polyfilled.js
diff options
context:
space:
mode:
Diffstat (limited to 'dist/plyr.polyfilled.js')
-rw-r--r--dist/plyr.polyfilled.js1046
1 files changed, 747 insertions, 299 deletions
diff --git a/dist/plyr.polyfilled.js b/dist/plyr.polyfilled.js
index eda5833e..ea9a5afa 100644
--- a/dist/plyr.polyfilled.js
+++ b/dist/plyr.polyfilled.js
@@ -1,4 +1,4 @@
-(function (global, factory) {
+typeof navigator === "object" && (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define('Plyr', factory) :
(global.Plyr = factory());
@@ -20,7 +20,7 @@ if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef
});
var _core = createCommonjsModule(function (module) {
-var core = module.exports = { version: '2.5.3' };
+var core = module.exports = { version: '2.5.6' };
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
});
var _core_1 = _core.version;
@@ -333,11 +333,18 @@ var _arrayIncludes = function (IS_INCLUDES) {
};
};
+var _shared = createCommonjsModule(function (module) {
var SHARED = '__core-js_shared__';
var store = _global[SHARED] || (_global[SHARED] = {});
-var _shared = function (key) {
- return store[key] || (store[key] = {});
-};
+
+(module.exports = function (key, value) {
+ return store[key] || (store[key] = value !== undefined ? value : {});
+})('versions', []).push({
+ version: _core.version,
+ mode: _library ? 'pure' : 'global',
+ copyright: '© 2018 Denis Pushkarev (zloirock.ru)'
+});
+});
var shared = _shared('keys');
@@ -991,7 +998,7 @@ var _iterDefine = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORC
var VALUES_BUG = false;
var proto = Base.prototype;
var $native = proto[ITERATOR$2] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT];
- var $default = (!BUGGY && $native) || getMethod(DEFAULT);
+ var $default = $native || getMethod(DEFAULT);
var $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined;
var $anyNative = NAME == 'Array' ? proto.entries || $native : $native;
var methods, key, IteratorPrototype;
@@ -1002,7 +1009,7 @@ var _iterDefine = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORC
// Set @@toStringTag to native iterators
_setToStringTag(IteratorPrototype, TAG, true);
// fix for some old engines
- if (!_library && !_has(IteratorPrototype, ITERATOR$2)) _hide(IteratorPrototype, ITERATOR$2, returnThis);
+ if (!_library && typeof IteratorPrototype[ITERATOR$2] != 'function') _hide(IteratorPrototype, ITERATOR$2, returnThis);
}
}
// fix Array#{values, @@iterator}.name in V8 / FF
@@ -2496,9 +2503,11 @@ function set(target, propertyKey, V /* , receiver */) {
}
if (_has(ownDesc, 'value')) {
if (ownDesc.writable === false || !_isObject(receiver)) return false;
- existingDescriptor = _objectGopd.f(receiver, propertyKey) || _propertyDesc(0);
- existingDescriptor.value = V;
- _objectDp.f(receiver, propertyKey, existingDescriptor);
+ if (existingDescriptor = _objectGopd.f(receiver, propertyKey)) {
+ if (existingDescriptor.get || existingDescriptor.set || existingDescriptor.writable === false) return false;
+ existingDescriptor.value = V;
+ _objectDp.f(receiver, propertyKey, existingDescriptor);
+ } else _objectDp.f(receiver, propertyKey, _propertyDesc(0, V));
return true;
}
return ownDesc.set === undefined ? false : (ownDesc.set.call(receiver, V), true);
@@ -2643,7 +2652,8 @@ var _microtask = function () {
};
// environments with maybe non-completely correct, but existent Promise
} else if (Promise$1 && Promise$1.resolve) {
- var promise = Promise$1.resolve();
+ // Promise.resolve without an argument throws an error in LG WebOS 2
+ var promise = Promise$1.resolve(undefined);
notify = function () {
promise.then(flush);
};
@@ -2700,6 +2710,10 @@ var _perform = function (exec) {
}
};
+var navigator$1 = _global.navigator;
+
+var _userAgent = navigator$1 && navigator$1.userAgent || '';
+
var _promiseResolve = function (C, x) {
_anObject(C);
if (_isObject(x) && x.constructor === C) return x;
@@ -2714,9 +2728,12 @@ var microtask = _microtask();
+
var PROMISE = 'Promise';
var TypeError$1 = _global.TypeError;
var process$2 = _global.process;
+var versions = process$2 && process$2.versions;
+var v8 = versions && versions.v8 || '';
var $Promise = _global[PROMISE];
var isNode$1 = _classof(process$2) == 'process';
var empty = function () { /* empty */ };
@@ -2731,7 +2748,13 @@ var USE_NATIVE = !!function () {
exec(empty, empty);
};
// unhandled rejections tracking support, NodeJS Promise without it fails @@species test
- return (isNode$1 || typeof PromiseRejectionEvent == 'function') && promise.then(empty) instanceof FakePromise;
+ return (isNode$1 || typeof PromiseRejectionEvent == 'function')
+ && promise.then(empty) instanceof FakePromise
+ // v8 6.6 (Node 10 and Chrome 66) have a bug with resolving custom thenables
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=830565
+ // we can't detect it synchronously, so just check versions
+ && v8.indexOf('6.6') !== 0
+ && _userAgent.indexOf('Chrome/66') === -1;
} catch (e) { /* empty */ }
}();
@@ -2753,7 +2776,7 @@ var notify = function (promise, isReject) {
var resolve = reaction.resolve;
var reject = reaction.reject;
var domain = reaction.domain;
- var result, then;
+ var result, then, exited;
try {
if (handler) {
if (!ok) {
@@ -2763,8 +2786,11 @@ var notify = function (promise, isReject) {
if (handler === true) result = value;
else {
if (domain) domain.enter();
- result = handler(value);
- if (domain) domain.exit();
+ result = handler(value); // may throw
+ if (domain) {
+ domain.exit();
+ exited = true;
+ }
}
if (result === reaction.promise) {
reject(TypeError$1('Promise-chain cycle'));
@@ -2773,6 +2799,7 @@ var notify = function (promise, isReject) {
} else resolve(result);
} else reject(value);
} catch (e) {
+ if (domain && !exited) domain.exit();
reject(e);
}
};
@@ -4155,10 +4182,6 @@ var _stringPad = function (that, maxLength, fillString, left) {
return left ? stringFiller + S : S + stringFiller;
};
-var navigator$1 = _global.navigator;
-
-var _userAgent = navigator$1 && navigator$1.userAgent || '';
-
// https://github.com/tc39/proposal-string-pad-start-end
@@ -5044,6 +5067,355 @@ try {
window.CustomEvent = CustomEvent$1; // expose definition to window
}
+(function(global) {
+ /**
+ * Polyfill URLSearchParams
+ *
+ * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js
+ */
+
+ var checkIfIteratorIsSupported = function() {
+ try {
+ return !!Symbol.iterator;
+ } catch(error) {
+ return false;
+ }
+ };
+
+
+ var iteratorSupported = checkIfIteratorIsSupported();
+
+ var createIterator = function(items) {
+ var iterator = {
+ next: function() {
+ var value = items.shift();
+ return { done: value === void 0, value: value };
+ }
+ };
+
+ if(iteratorSupported) {
+ iterator[Symbol.iterator] = function() {
+ return iterator;
+ };
+ }
+
+ return iterator;
+ };
+
+ /**
+ * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing
+ * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.
+ */
+ var serializeParam = function(value) {
+ return encodeURIComponent(value).replace(/%20/g, '+');
+ };
+
+ var deserializeParam = function(value) {
+ return decodeURIComponent(value).replace(/\+/g, ' ');
+ };
+
+ var polyfillURLSearchParams= function() {
+
+ var URLSearchParams = function(searchString) {
+ Object.defineProperty(this, '_entries', { value: {} });
+
+ if(typeof searchString === 'string') {
+ if(searchString !== '') {
+ searchString = searchString.replace(/^\?/, '');
+ var attributes = searchString.split('&');
+ var attribute;
+ for(var i = 0; i < attributes.length; i++) {
+ attribute = attributes[i].split('=');
+ this.append(
+ deserializeParam(attribute[0]),
+ (attribute.length > 1) ? deserializeParam(attribute[1]) : ''
+ );
+ }
+ }
+ } else if(searchString instanceof URLSearchParams) {
+ var _this = this;
+ searchString.forEach(function(value, name) {
+ _this.append(value, name);
+ });
+ }
+ };
+
+ var proto = URLSearchParams.prototype;
+
+ proto.append = function(name, value) {
+ if(name in this._entries) {
+ this._entries[name].push(value.toString());
+ } else {
+ this._entries[name] = [value.toString()];
+ }
+ };
+
+ proto.delete = function(name) {
+ delete this._entries[name];
+ };
+
+ proto.get = function(name) {
+ return (name in this._entries) ? this._entries[name][0] : null;
+ };
+
+ proto.getAll = function(name) {
+ return (name in this._entries) ? this._entries[name].slice(0) : [];
+ };
+
+ proto.has = function(name) {
+ return (name in this._entries);
+ };
+
+ proto.set = function(name, value) {
+ this._entries[name] = [value.toString()];
+ };
+
+ proto.forEach = function(callback, thisArg) {
+ var entries;
+ for(var name in this._entries) {
+ if(this._entries.hasOwnProperty(name)) {
+ entries = this._entries[name];
+ for(var i = 0; i < entries.length; i++) {
+ callback.call(thisArg, entries[i], name, this);
+ }
+ }
+ }
+ };
+
+ proto.keys = function() {
+ var items = [];
+ this.forEach(function(value, name) { items.push(name); });
+ return createIterator(items);
+ };
+
+ proto.values = function() {
+ var items = [];
+ this.forEach(function(value) { items.push(value); });
+ return createIterator(items);
+ };
+
+ proto.entries = function() {
+ var items = [];
+ this.forEach(function(value, name) { items.push([name, value]); });
+ return createIterator(items);
+ };
+
+ if(iteratorSupported) {
+ proto[Symbol.iterator] = proto.entries;
+ }
+
+ proto.toString = function() {
+ var searchString = '';
+ this.forEach(function(value, name) {
+ if(searchString.length > 0) searchString+= '&';
+ searchString += serializeParam(name) + '=' + serializeParam(value);
+ });
+ return searchString;
+ };
+
+ global.URLSearchParams = URLSearchParams;
+ };
+
+ if(!('URLSearchParams' in global) || (new URLSearchParams('?a=1').toString() !== 'a=1')) {
+ polyfillURLSearchParams();
+ }
+
+ // HTMLAnchorElement
+
+})(
+ (typeof commonjsGlobal !== 'undefined') ? commonjsGlobal
+ : ((typeof window !== 'undefined') ? window
+ : ((typeof self !== 'undefined') ? self : commonjsGlobal))
+);
+
+(function(global) {
+ /**
+ * Polyfill URL
+ *
+ * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js
+ */
+
+ var checkIfURLIsSupported = function() {
+ try {
+ var u = new URL('b', 'http://a');
+ u.pathname = 'c%20d';
+ return (u.href === 'http://a/c%20d') && u.searchParams;
+ } catch(e) {
+ return false;
+ }
+ };
+
+
+ var polyfillURL = function() {
+ var _URL = global.URL;
+
+ var URL = function(url, base) {
+ if(typeof url !== 'string') url = String(url);
+
+ var doc = document.implementation.createHTMLDocument('');
+ window.doc = doc;
+ if(base) {
+ var baseElement = doc.createElement('base');
+ baseElement.href = base;
+ doc.head.appendChild(baseElement);
+ }
+
+ var anchorElement = doc.createElement('a');
+ anchorElement.href = url;
+ doc.body.appendChild(anchorElement);
+ anchorElement.href = anchorElement.href; // force href to refresh
+
+ if(anchorElement.protocol === ':' || !/:/.test(anchorElement.href)) {
+ throw new TypeError('Invalid URL');
+ }
+
+ Object.defineProperty(this, '_anchorElement', {
+ value: anchorElement
+ });
+ };
+
+ var proto = URL.prototype;
+
+ var linkURLWithAnchorAttribute = function(attributeName) {
+ Object.defineProperty(proto, attributeName, {
+ get: function() {
+ return this._anchorElement[attributeName];
+ },
+ set: function(value) {
+ this._anchorElement[attributeName] = value;
+ },
+ enumerable: true
+ });
+ };
+
+ ['hash', 'host', 'hostname', 'port', 'protocol', 'search']
+ .forEach(function(attributeName) {
+ linkURLWithAnchorAttribute(attributeName);
+ });
+
+ Object.defineProperties(proto, {
+
+ 'toString': {
+ get: function() {
+ var _this = this;
+ return function() {
+ return _this.href;
+ };
+ }
+ },
+
+ 'href' : {
+ get: function() {
+ return this._anchorElement.href.replace(/\?$/,'');
+ },
+ set: function(value) {
+ this._anchorElement.href = value;
+ },
+ enumerable: true
+ },
+
+ 'pathname' : {
+ get: function() {
+ return this._anchorElement.pathname.replace(/(^\/?)/,'/');
+ },
+ set: function(value) {
+ this._anchorElement.pathname = value;
+ },
+ enumerable: true
+ },
+
+ 'origin': {
+ get: function() {
+ // get expected port from protocol
+ var expectedPort = {'http:': 80, 'https:': 443, 'ftp:': 21}[this._anchorElement.protocol];
+ // add port to origin if, expected port is different than actual port
+ // and it is not empty f.e http://foo:8080
+ // 8080 != 80 && 8080 != ''
+ var addPortToOrigin = this._anchorElement.port != expectedPort &&
+ this._anchorElement.port !== '';
+
+ return this._anchorElement.protocol +
+ '//' +
+ this._anchorElement.hostname +
+ (addPortToOrigin ? (':' + this._anchorElement.port) : '');
+ },
+ enumerable: true
+ },
+
+ 'password': { // TODO
+ get: function() {
+ return '';
+ },
+ set: function(value) {
+ },
+ enumerable: true
+ },
+
+ 'username': { // TODO
+ get: function() {
+ return '';
+ },
+ set: function(value) {
+ },
+ enumerable: true
+ },
+
+ 'searchParams': {
+ get: function() {
+ var searchParams = new URLSearchParams(this.search);
+ var _this = this;
+ ['append', 'delete', 'set'].forEach(function(methodName) {
+ var method = searchParams[methodName];
+ searchParams[methodName] = function() {
+ method.apply(searchParams, arguments);
+ _this.search = searchParams.toString();
+ };
+ });
+ return searchParams;
+ },
+ enumerable: true
+ }
+ });
+
+ URL.createObjectURL = function(blob) {
+ return _URL.createObjectURL.apply(_URL, arguments);
+ };
+
+ URL.revokeObjectURL = function(url) {
+ return _URL.revokeObjectURL.apply(_URL, arguments);
+ };
+
+ global.URL = URL;
+
+ };
+
+ if(!checkIfURLIsSupported()) {
+ polyfillURL();
+ }
+
+ if((global.location !== void 0) && !('origin' in global.location)) {
+ var getOrigin = function() {
+ return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');
+ };
+
+ try {
+ Object.defineProperty(global.location, 'origin', {
+ get: getOrigin,
+ enumerable: true
+ });
+ } catch(e) {
+ setInterval(function() {
+ global.location.origin = getOrigin();
+ }, 100);
+ }
+ }
+
+})(
+ (typeof commonjsGlobal !== 'undefined') ? commonjsGlobal
+ : ((typeof window !== 'undefined') ? window
+ : ((typeof self !== 'undefined') ? self : commonjsGlobal))
+);
+
var loadjs_umd = createCommonjsModule(function (module, exports) {
(function(root, factory) {
if (typeof undefined === 'function' && undefined.amd) {
@@ -5636,6 +6008,24 @@ var utils = {
},
+ // Load image avoiding xhr/fetch CORS issues
+ // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded.
+ // By default it checks if it is at least 1px, but you can add a second argument to change this.
+ loadImage: function loadImage(src) {
+ var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+
+ return new Promise(function (resolve, reject) {
+ var image = new Image();
+ var handler = function handler() {
+ delete image.onload;
+ delete image.onerror;
+ (image.naturalWidth >= minWidth ? resolve : reject)(image);
+ };
+ Object.assign(image, { onload: handler, onerror: handler, src: src });
+ });
+ },
+
+
// Load an external script
loadScript: function loadScript(url) {
return new Promise(function (resolve, reject) {
@@ -5764,7 +6154,7 @@ var utils = {
// Add text node
if (utils.is.string(text)) {
- element.textContent = text;
+ element.innerText = text;
}
// Return built element
@@ -5918,14 +6308,16 @@ var utils = {
},
- // Toggle class on an element
- toggleClass: function toggleClass(element, className, toggle) {
+ // Mirror Element.classList.toggle, with IE compatibility for "force" argument
+ toggleClass: function toggleClass(element, className, force) {
if (utils.is.element(element)) {
- var contains = element.classList.contains(className);
-
- element.classList[toggle ? 'add' : 'remove'](className);
+ var method = 'toggle';
+ if (typeof force !== 'undefined') {
+ method = force ? 'add' : 'remove';
+ }
- return toggle && !contains || !toggle && contains;
+ element.classList[method](className);
+ return element.classList.contains(className);
}
return null;
@@ -6308,6 +6700,12 @@ var utils = {
},
+ // Clone nested objects
+ cloneDeep: function cloneDeep(object) {
+ return JSON.parse(JSON.stringify(object));
+ },
+
+
// Get the closest value in an array
closest: function closest(array, value) {
if (!utils.is.array(array) || !array.length) {
@@ -7127,7 +7525,7 @@ var controls = {
break;
}
- progress.textContent = '% ' + suffix.toLowerCase();
+ progress.innerText = '% ' + suffix.toLowerCase();
}
this.elements.display[type] = progress;
@@ -7201,7 +7599,7 @@ var controls = {
var forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
- target.textContent = utils.formatTime(time, forceHours, inverted);
+ target.innerText = utils.formatTime(time, forceHours, inverted);
},
@@ -7270,6 +7668,7 @@ var controls = {
// Video playing
case 'timeupdate':
case 'seeking':
+ case 'seeked':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
@@ -7327,7 +7726,7 @@ var controls = {
// Calculate percentage
var percent = 0;
- var clientRect = this.elements.inputs.seek.getBoundingClientRect();
+ var clientRect = this.elements.progress.getBoundingClientRect();
var visible = this.config.classNames.tooltip + '--visible';
var toggle = function toggle(_toggle) {
@@ -7388,9 +7787,10 @@ var controls = {
},
- // Show the duration on metadataloaded
+ // Show the duration on metadataloaded or durationchange events
durationUpdate: function durationUpdate() {
- if (!this.supported.ui) {
+ // Bail if no ui or durationchange event triggered after playing/seek when invertTime is false
+ if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
return;
}
@@ -7954,7 +8354,6 @@ var controls = {
// Seek tooltip
if (this.config.tooltips.seek) {
var tooltip = utils.createElement('span', {
- role: 'tooltip',
class: this.config.classNames.tooltip
}, '00:00');
@@ -8505,7 +8904,7 @@ var captions = {
// Set the span content
if (utils.is.string(caption)) {
- content.textContent = caption.trim();
+ content.innerText = caption.trim();
} else {
content.appendChild(caption);
}
@@ -8635,7 +9034,7 @@ var defaults$1 = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
- iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg',
+ iconUrl: 'https://cdn.plyr.io/3.3.8/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -8743,8 +9142,7 @@ var defaults$1 = {
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
- api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
- poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg'
+ api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet'
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
@@ -8840,13 +9238,13 @@ var defaults$1 = {
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
+ posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
- error: 'plyr--has-error',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
@@ -9241,8 +9639,10 @@ var ui = {
// Set the title
ui.setTitle.call(this);
- // Set the poster image
- ui.setPoster.call(this);
+ // Assure the poster image is set, if the property was added before the element was created
+ if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
+ ui.setPoster.call(this, this.poster);
+ }
},
@@ -9284,17 +9684,43 @@ var ui = {
},
- // Set the poster image
- setPoster: function setPoster() {
- if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
- return;
+ // Toggle poster
+ togglePoster: function togglePoster(enable) {
+ utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
+ },
+
+
+ // Set the poster image (async)
+ setPoster: function setPoster(poster) {
+ var _this2 = this;
+
+ // Set property regardless of validity
+ this.media.setAttribute('poster', poster);
+
+ // Bail if element is missing
+ if (!utils.is.element(this.elements.poster)) {
+ return Promise.reject();
}
- // Set the inline style
- var posters = this.poster.split(',');
- this.elements.poster.style.backgroundImage = posters.map(function (p) {
- return 'url(\'' + p + '\')';
- }).join(',');
+ // Load the image, and set poster if successful
+ var loadPromise = utils.loadImage(poster).then(function () {
+ _this2.elements.poster.style.backgroundImage = 'url(\'' + poster + '\')';
+ Object.assign(_this2.elements.poster.style, {
+ backgroundImage: 'url(\'' + poster + '\')',
+ // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
+ backgroundSize: ''
+ });
+ ui.togglePoster.call(_this2, true);
+ return poster;
+ });
+
+ // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
+ loadPromise.catch(function () {
+ return ui.togglePoster.call(_this2, false);
+ });
+
+ // Return the promise so the caller can use it as well
+ return loadPromise;
},
@@ -9314,13 +9740,13 @@ var ui = {
}
// Toggle controls
- this.toggleControls(!this.playing);
+ ui.toggleControls.call(this);
},
// Check if media is loading
checkLoading: function checkLoading(event) {
- var _this2 = this;
+ var _this3 = this;
this.loading = ['stalled', 'waiting'].includes(event.type);
@@ -9329,38 +9755,24 @@ var ui = {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(function () {
- // Toggle container class hook
- utils.toggleClass(_this2.elements.container, _this2.config.classNames.loading, _this2.loading);
+ // Update progress bar loading class state
+ utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
- // Show controls if loading, hide if done
- _this2.toggleControls(_this2.loading);
+ // Update controls visibility
+ ui.toggleControls.call(_this3);
}, this.loading ? 250 : 0);
},
- // Check if media failed to load
- checkFailed: function checkFailed() {
- var _this3 = this;
+ // Toggle controls based on state and `force` argument
+ toggleControls: function toggleControls(force) {
+ var controls$$1 = this.elements.controls;
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
- this.failed = this.media.networkState === 3;
- if (this.failed) {
- utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
- utils.toggleClass(this.elements.container, this.config.classNames.error, true);
+ 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));
}
-
- // Clear timer
- clearTimeout(this.timers.failed);
-
- // Timer to prevent flicker when seeking
- this.timers.loading = setTimeout(function () {
- // Toggle container class hook
- utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
-
- // Show controls if loading, hide if done
- _this3.toggleControls(_this3.loading);
- }, this.loading ? 250 : 0);
}
};
@@ -9598,13 +10010,35 @@ var Listeners = function () {
}, 0);
});
- // Toggle controls visibility based on mouse movement
- if (this.player.config.hideControls) {
- // Toggle controls on mouse events and entering fullscreen
- utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) {
- _this2.player.toggleControls(event);
- });
- }
+ // Toggle controls on mouse events and entering fullscreen
+ utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
+ var controls$$1 = _this2.player.elements.controls;
+
+ // Remove button states for fullscreen
+
+ if (event.type === 'enterfullscreen') {
+ controls$$1.pressed = false;
+ controls$$1.hover = false;
+ }
+
+ // Show, then hide after a timeout unless another control event occurs
+ var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
+
+ var delay = 0;
+
+ if (show) {
+ ui.toggleControls.call(_this2.player, true);
+ // Use longer timeout for touch devices
+ delay = _this2.player.touch ? 3000 : 2000;
+ }
+
+ // Clear timer
+ clearTimeout(_this2.player.timers.controls);
+ // Timer to prevent flicker when seeking
+ _this2.player.timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(_this2.player, false);
+ }, delay);
+ });
}
// Listen for media events
@@ -9615,7 +10049,7 @@ var Listeners = function () {
var _this3 = this;
// Time change on media
- utils.on(this.player.media, 'timeupdate seeking', function (event) {
+ utils.on(this.player.media, 'timeupdate seeking seeked', function (event) {
return controls.timeUpdate.call(_this3.player, event);
});
@@ -9641,7 +10075,7 @@ var Listeners = function () {
});
// Check for buffer progress
- utils.on(this.player.media, 'progress playing', function (event) {
+ utils.on(this.player.media, 'progress playing seeking seeked', function (event) {
return controls.updateProgress.call(_this3.player, event);
});
@@ -9660,9 +10094,6 @@ var Listeners = function () {
return ui.checkLoading.call(_this3.player, event);
});
- // Check if media failed to load
- // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
-
// If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
utils.on(this.player.media, 'playing', function () {
@@ -9884,9 +10315,47 @@ var Listeners = function () {
}
});
+ // Set range input alternative "value", which matches the tooltip time (#954)
+ on(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) {
+ var clientRect = _this4.player.elements.progress.getBoundingClientRect();
+ var percent = 100 / clientRect.width * (event.pageX - clientRect.left);
+ event.currentTarget.setAttribute('seek-value', percent);
+ });
+
+ // Pause while seeking
+ on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) {
+ var seek = event.currentTarget;
+
+ // Was playing before?
+ var play = seek.hasAttribute('play-on-seeked');
+
+ // Done seeking
+ var done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
+
+ // If we're done seeking and it was playing, resume playback
+ if (play && done) {
+ seek.removeAttribute('play-on-seeked');
+ _this4.player.play();
+ } else if (!done && _this4.player.playing) {
+ seek.setAttribute('play-on-seeked', '');
+ _this4.player.pause();
+ }
+ });
+
// Seek
on(this.player.elements.inputs.seek, inputEvent, function (event) {
- _this4.player.currentTime = event.target.value / event.target.max * _this4.player.duration;
+ var seek = event.currentTarget;
+
+ // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
+ var seekTo = seek.getAttribute('seek-value');
+
+ if (utils.is.empty(seekTo)) {
+ seekTo = seek.value;
+ }
+
+ seek.removeAttribute('seek-value');
+
+ _this4.player.currentTime = seekTo / seek.max * _this4.player.duration;
}, 'seek');
// Current time invert
@@ -9921,23 +10390,48 @@ var Listeners = function () {
return controls.updateSeekTooltip.call(_this4.player, event);
});
- // Toggle controls visibility based on mouse movement
- if (this.player.config.hideControls) {
- // Watch for cursor over controls so they don't hide when trying to interact
- on(this.player.elements.controls, 'mouseenter mouseleave', function (event) {
- _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter';
- });
+ // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
+ on(this.player.elements.controls, 'mouseenter mouseleave', function (event) {
+ _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter';
+ });
- // Watch for cursor over controls so they don't hide when trying to interact
- on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
- _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
- });
+ // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
+ on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
+ _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
+ });
- // Focus in/out on controls
- on(this.player.elements.controls, 'focusin focusout', function (event) {
- _this4.player.toggleControls(event);
- });
- }
+ // Focus in/out on controls
+ on(this.player.elements.controls, 'focusin focusout', function (event) {
+ var _player = _this4.player,
+ config = _player.config,
+ elements = _player.elements,
+ timers = _player.timers;
+
+ // Skip transition to prevent focus from scrolling the parent element
+
+ utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
+
+ // Toggle
+ ui.toggleControls.call(_this4.player, event.type === 'focusin');
+
+ // If focusin, hide again after delay
+ if (event.type === 'focusin') {
+ // Restore transition
+ setTimeout(function () {
+ utils.toggleClass(elements.controls, config.classNames.noTransition, false);
+ }, 0);
+
+ // Delay a little more for keyboard users
+ var delay = _this4.touch ? 3000 : 4000;
+
+ // Clear timer
+ clearTimeout(timers.controls);
+ // Hide
+ timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(_this4.player, false);
+ }, delay);
+ }
+ });
// Mouse wheel for volume
on(this.player.elements.inputs.volume, 'wheel', function (event) {
@@ -9989,6 +10483,14 @@ var Listeners = function () {
// ==========================================================================
+// Set playback state and trigger change (only on actual change)
+function assurePlaybackState(play) {
+ if (this.media.paused === play) {
+ this.media.paused = !play;
+ utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ }
+}
+
var vimeo = {
setup: function setup() {
var _this = this;
@@ -10084,11 +10586,8 @@ var vimeo = {
// Get original image
url.pathname = url.pathname.split('_')[0] + '.jpg';
- // Set attribute
- player.media.setAttribute('poster', url.href);
-
- // Update
- ui.setPoster.call(player);
+ // Set and show poster
+ ui.setPoster.call(player, url.href);
});
// Setup instance
@@ -10108,15 +10607,13 @@ var vimeo = {
// Create a faux HTML5 API using the Vimeo API
player.media.play = function () {
- player.embed.play().then(function () {
- player.media.paused = false;
- });
+ assurePlaybackState.call(player, true);
+ return player.embed.play();
};
player.media.pause = function () {
- player.embed.pause().then(function () {
- player.media.paused = true;
- });
+ assurePlaybackState.call(player, false);
+ return player.embed.pause();
};
player.media.stop = function () {
@@ -10132,26 +10629,35 @@ var vimeo = {
return currentTime;
},
set: function set(time) {
- // Get current paused state
- // Vimeo will automatically play on seek
- var paused = player.media.paused;
-
- // Set seeking flag
-
- player.media.seeking = true;
-
- // Trigger seeking
- utils.dispatchEvent.call(player, player.media, 'seeking');
-
- // Seek after events
- player.embed.setCurrentTime(time).catch(function () {
+ // Vimeo will automatically play on seek if the video hasn't been played before
+
+ // Get current paused state and volume etc
+ var embed = player.embed,
+ media = player.media,
+ paused = player.paused,
+ volume = player.volume;
+
+ // Set seeking state and trigger event
+
+ media.seeking = true;
+ utils.dispatchEvent.call(player, media, 'seeking');
+
+ // If paused, mute until seek is complete
+ Promise.resolve(paused && embed.setVolume(0))
+ // Seek
+ .then(function () {
+ return embed.setCurrentTime(time);
+ })
+ // Restore paused
+ .then(function () {
+ return paused && embed.pause();
+ })
+ // Restore volume
+ .then(function () {
+ return paused && embed.setVolume(volume);
+ }).catch(function () {
// Do nothing
});
-
- // Restore pause state
- if (paused) {
- player.pause();
- }
}
});
@@ -10299,17 +10805,12 @@ var vimeo = {
});
player.embed.on('play', function () {
- // Only fire play if paused before
- if (player.media.paused) {
- utils.dispatchEvent.call(player, player.media, 'play');
- }
- player.media.paused = false;
+ assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', function () {
- player.media.paused = true;
- utils.dispatchEvent.call(player, player.media, 'pause');
+ assurePlaybackState.call(player, false);
});
player.embed.on('timeupdate', function (data) {
@@ -10340,7 +10841,6 @@ var vimeo = {
player.embed.on('seeked', function () {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
- utils.dispatchEvent.call(player, player.media, 'play');
});
player.embed.on('ended', function () {
@@ -10422,6 +10922,14 @@ function mapQualityUnits(levels) {
}));
}
+// Set playback state and trigger change (only on actual change)
+function assurePlaybackState$1(play) {
+ if (this.media.paused === play) {
+ this.media.paused = !play;
+ utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ }
+}
+
var youtube = {
setup: function setup() {
var _this = this;
@@ -10525,7 +11033,26 @@ var youtube = {
player.media = utils.replaceElement(container, player.media);
// Set poster image
- player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
+ var posterSrc = function posterSrc(format) {
+ return 'https://img.youtube.com/vi/' + videoId + '/' + format + 'default.jpg';
+ };
+
+ // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
+ utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
+ .catch(function () {
+ return utils.loadImage(posterSrc('sd'), 121);
+ }) // 480p padded 4:3
+ .catch(function () {
+ return utils.loadImage(posterSrc('hq'));
+ }) // 360p padded 4:3. Always exists
+ .then(function (image) {
+ return ui.setPoster.call(player, image.src);
+ }).then(function (posterSrc) {
+ // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
+ if (!posterSrc.includes('maxres')) {
+ player.elements.poster.style.backgroundSize = 'cover';
+ }
+ });
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
@@ -10612,10 +11139,12 @@ var youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = function () {
+ assurePlaybackState$1.call(player, true);
instance.playVideo();
};
player.media.pause = function () {
+ assurePlaybackState$1.call(player, false);
instance.pauseVideo();
};
@@ -10633,23 +11162,17 @@ var youtube = {
return Number(instance.getCurrentTime());
},
set: function set(time) {
- // Vimeo will automatically play on seek
- var paused = player.media.paused;
-
- // Set seeking flag
+ // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
+ if (player.paused) {
+ player.embed.mute();
+ }
+ // Set seeking state and trigger event
player.media.seeking = true;
-
- // Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
-
- // Restore pause state
- if (paused) {
- player.pause();
- }
}
});
@@ -10772,6 +11295,14 @@ var youtube = {
// Reset timer
clearInterval(player.timers.playing);
+ var seeked = player.media.seeking && [1, 2].includes(event.data);
+
+ if (seeked) {
+ // Unset seeking and fire seeked event
+ player.media.seeking = false;
+ utils.dispatchEvent.call(player, player.media, 'seeked');
+ }
+
// Handle events
// -1 Unstarted
// 0 Ended
@@ -10791,7 +11322,7 @@ var youtube = {
break;
case 0:
- player.media.paused = true;
+ assurePlaybackState$1.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@@ -10805,42 +11336,39 @@ var youtube = {
break;
case 1:
- // If we were seeking, fire seeked event
- if (player.media.seeking) {
- utils.dispatchEvent.call(player, player.media, 'seeked');
- }
- player.media.seeking = false;
-
- // Only fire play if paused before
+ // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused) {
- utils.dispatchEvent.call(player, player.media, 'play');
- }
- player.media.paused = false;
+ player.media.pause();
+ } else {
+ assurePlaybackState$1.call(player, true);
- utils.dispatchEvent.call(player, player.media, 'playing');
+ utils.dispatchEvent.call(player, player.media, 'playing');
- // Poll to get playback progress
- player.timers.playing = setInterval(function () {
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- }, 50);
+ // Poll to get playback progress
+ player.timers.playing = setInterval(function () {
+ utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ }, 50);
- // Check duration again due to YouTube bug
- // https://github.com/sampotts/plyr/issues/374
- // https://code.google.com/p/gdata-issues/issues/detail?id=8690
- if (player.media.duration !== instance.getDuration()) {
- player.media.duration = instance.getDuration();
- utils.dispatchEvent.call(player, player.media, 'durationchange');
- }
+ // Check duration again due to YouTube bug
+ // https://github.com/sampotts/plyr/issues/374
+ // https://code.google.com/p/gdata-issues/issues/detail?id=8690
+ if (player.media.duration !== instance.getDuration()) {
+ player.media.duration = instance.getDuration();
+ utils.dispatchEvent.call(player, player.media, 'durationchange');
+ }
- // Get quality
- controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
+ // Get quality
+ controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
+ }
break;
case 2:
- player.media.paused = true;
-
- utils.dispatchEvent.call(player, player.media, 'pause');
+ // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
+ if (!player.muted) {
+ player.embed.unMute();
+ }
+ assurePlaybackState$1.call(player, false);
break;
@@ -11774,7 +12302,7 @@ var Plyr = function () {
}
// Set config
- this.config = utils.extend({}, defaults$1, options || {}, function () {
+ this.config = utils.extend({}, defaults$1, Plyr.defaults, options || {}, function () {
try {
return JSON.parse(_this.media.getAttribute('data-plyr-config'));
} catch (e) {
@@ -12214,114 +12742,35 @@ var Plyr = function () {
/**
* Toggle the player controls
- * @param {boolean} toggle - Whether to show the controls
+ * @param {boolean} [toggle] - Whether to show the controls
*/
}, {
key: 'toggleControls',
value: function toggleControls(toggle) {
- var _this2 = this;
+ // Don't toggle if missing UI support or if it's audio
+ if (this.supported.ui && !this.isAudio) {
+ // Get state before change
+ var isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
- // We need controls of course...
- if (!utils.is.element(this.elements.controls)) {
- return;
- }
-
- // Don't hide if no UI support or it's audio
- if (!this.supported.ui || this.isAudio) {
- return;
- }
-
- var delay = 0;
- var show = toggle;
- var isEnterFullscreen = false;
-
- // Get toggle state if not set
- if (!utils.is.boolean(toggle)) {
- if (utils.is.event(toggle)) {
- // Is the enter fullscreen event
- isEnterFullscreen = toggle.type === 'enterfullscreen';
-
- // Events that show the controls
- var showEvents = ['touchstart', 'touchmove', 'mouseenter', 'mousemove', 'focusin'];
-
- // Events that delay hiding
- var delayEvents = ['touchmove', 'touchend', 'mousemove'];
-
- // Whether to show controls
- show = showEvents.includes(toggle.type);
+ // Negate the argument if not undefined since adding the class to hides the controls
+ var force = typeof toggle === 'undefined' ? undefined : !toggle;
- // Delay hiding on move events
- if (delayEvents.includes(toggle.type)) {
- delay = 2000;
- }
-
- // Delay a little more for keyboard users
- if (!this.touch && toggle.type === 'focusin') {
- delay = 3000;
- utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
- }
- } else {
- show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
- }
- }
-
- // Clear timer on every call
- clearTimeout(this.timers.controls);
-
- // If the mouse is not over the controls, set a timeout to hide them
- if (show || this.paused || this.loading) {
- // Check if controls toggled
- var toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
-
- // Trigger event
- if (toggled) {
- utils.dispatchEvent.call(this, this.media, 'controlsshown');
- }
+ // Apply and get updated state
+ var hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
- // Always show controls when paused or if touch
- if (this.paused || this.loading) {
- return;
+ // Close menu
+ if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
+ controls.toggleMenu.call(this, false);
}
-
- // Delay for hiding on touch
- if (this.touch) {
- delay = 3000;
+ // Trigger event on change
+ if (hiding !== isHidden) {
+ var eventName = hiding ? 'controlshidden' : 'controlsshown';
+ utils.dispatchEvent.call(this, this.media, eventName);
}
+ return !hiding;
}
-
- // If toggle is false or if we're playing (regardless of toggle),
- // then set the timer to hide the controls
- if (!show || this.playing) {
- this.timers.controls = setTimeout(function () {
- // We need controls of course...
- if (!utils.is.element(_this2.elements.controls)) {
- return;
- }
-
- // If the mouse is over the controls (and not entering fullscreen), bail
- if ((_this2.elements.controls.pressed || _this2.elements.controls.hover) && !isEnterFullscreen) {
- return;
- }
-
- // Restore transition behaviour
- if (!utils.hasClass(_this2.elements.container, _this2.config.classNames.hideControls)) {
- utils.toggleClass(_this2.elements.controls, _this2.config.classNames.noTransition, false);
- }
-
- // Set hideControls class
- var toggled = utils.toggleClass(_this2.elements.container, _this2.config.classNames.hideControls, _this2.config.hideControls);
-
- // Trigger event and close menu
- if (toggled) {
- utils.dispatchEvent.call(_this2, _this2.media, 'controlshidden');
-
- if (_this2.config.controls.includes('settings') && !utils.is.empty(_this2.config.settings)) {
- controls.toggleMenu.call(_this2, false);
- }
- }
- }, delay);
- }
+ return false;
}
/**
@@ -12359,7 +12808,7 @@ var Plyr = function () {
}, {
key: 'destroy',
value: function destroy(callback) {
- var _this3 = this;
+ var _this2 = this;
var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
@@ -12372,22 +12821,22 @@ var Plyr = function () {
document.body.style.overflow = '';
// GC for embed
- _this3.embed = null;
+ _this2.embed = null;
// If it's a soft destroy, make minimal changes
if (soft) {
- if (Object.keys(_this3.elements).length) {
+ if (Object.keys(_this2.elements).length) {
// Remove elements
- utils.removeElement(_this3.elements.buttons.play);
- utils.removeElement(_this3.elements.captions);
- utils.removeElement(_this3.elements.controls);
- utils.removeElement(_this3.elements.wrapper);
+ utils.removeElement(_this2.elements.buttons.play);
+ utils.removeElement(_this2.elements.captions);
+ utils.removeElement(_this2.elements.controls);
+ utils.removeElement(_this2.elements.wrapper);
// Clear for GC
- _this3.elements.buttons.play = null;
- _this3.elements.captions = null;
- _this3.elements.controls = null;
- _this3.elements.wrapper = null;
+ _this2.elements.buttons.play = null;
+ _this2.elements.captions = null;
+ _this2.elements.controls = null;
+ _this2.elements.wrapper = null;
}
// Callback
@@ -12396,26 +12845,26 @@ var Plyr = function () {
}
} else {
// Unbind listeners
- _this3.listeners.clear();
+ _this2.listeners.clear();
// Replace the container with the original element provided
- utils.replaceElement(_this3.elements.original, _this3.elements.container);
+ utils.replaceElement(_this2.elements.original, _this2.elements.container);
// Event
- utils.dispatchEvent.call(_this3, _this3.elements.original, 'destroyed', true);
+ utils.dispatchEvent.call(_this2, _this2.elements.original, 'destroyed', true);
// Callback
if (utils.is.function(callback)) {
- callback.call(_this3.elements.original);
+ callback.call(_this2.elements.original);
}
// Reset state
- _this3.ready = false;
+ _this2.ready = false;
// Clear for garbage collection
setTimeout(function () {
- _this3.elements = null;
- _this3.media = null;
+ _this2.elements = null;
+ _this2.media = null;
}, 200);
}
};
@@ -12934,10 +13383,7 @@ var Plyr = function () {
return;
}
- if (utils.is.string(input)) {
- this.media.setAttribute('poster', input);
- ui.setPoster.call(this);
- }
+ ui.setPoster.call(this, input);
}
/**
@@ -13118,6 +13564,8 @@ var Plyr = function () {
return Plyr;
}();
+Plyr.defaults = utils.cloneDeep(defaults$1);
+
// ==========================================================================
return Plyr;