@supercast/plyr
Version:
A simple, accessible and customizable HTML5, YouTube and Vimeo media player
1,499 lines (1,393 loc) • 318 kB
JavaScript
typeof navigator === "object" && (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define('Plyr', factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());
})(this, (function () { 'use strict';
// Polyfill for creating CustomEvents on IE9/10/11
// code pulled from:
// https://github.com/d4tocchini/customevent-polyfill
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill
(function () {
if (typeof window === 'undefined') {
return;
}
try {
var ce = new window.CustomEvent('test', {
cancelable: true
});
ce.preventDefault();
if (ce.defaultPrevented !== true) {
// IE has problems with .preventDefault() on custom events
// http://stackoverflow.com/questions/23349191
throw new Error('Could not prevent default');
}
} catch (e) {
var CustomEvent = function (event, params) {
var evt, origPrevent;
params = params || {};
params.bubbles = !!params.bubbles;
params.cancelable = !!params.cancelable;
evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
origPrevent = evt.preventDefault;
evt.preventDefault = function () {
origPrevent.call(this);
try {
Object.defineProperty(this, 'defaultPrevented', {
get: function () {
return true;
}
});
} catch (e) {
this.defaultPrevented = true;
}
};
return evt;
};
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent; // expose definition to window
}
})();
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
(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(String(value).replace(/\+/g, ' '));
};
var polyfillURLSearchParams = function () {
var URLSearchParams = function (searchString) {
Object.defineProperty(this, '_entries', {
writable: true,
value: {}
});
var typeofSearchString = typeof searchString;
if (typeofSearchString === 'undefined') ; else if (typeofSearchString === 'string') {
if (searchString !== '') {
this._fromString(searchString);
}
} else if (searchString instanceof URLSearchParams) {
var _this = this;
searchString.forEach(function (value, name) {
_this.append(name, value);
});
} else if (searchString !== null && typeofSearchString === 'object') {
if (Object.prototype.toString.call(searchString) === '[object Array]') {
for (var i = 0; i < searchString.length; i++) {
var entry = searchString[i];
if (Object.prototype.toString.call(entry) === '[object Array]' || entry.length !== 2) {
this.append(entry[0], entry[1]);
} else {
throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\'s input');
}
}
} else {
for (var key in searchString) {
if (searchString.hasOwnProperty(key)) {
this.append(key, searchString[key]);
}
}
}
} else {
throw new TypeError('Unsupported input\'s type for URLSearchParams');
}
};
var proto = URLSearchParams.prototype;
proto.append = function (name, value) {
if (name in this._entries) {
this._entries[name].push(String(value));
} else {
this._entries[name] = [String(value)];
}
};
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] = [String(value)];
};
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 searchArray = [];
this.forEach(function (value, name) {
searchArray.push(serializeParam(name) + '=' + serializeParam(value));
});
return searchArray.join('&');
};
Object.defineProperty(proto, 'size', {
get: function () {
return this._entries ? Object.keys(this._entries).length : 0;
}
});
global.URLSearchParams = URLSearchParams;
};
var checkIfURLSearchParamsSupported = function () {
try {
var URLSearchParams = global.URLSearchParams;
return new URLSearchParams('?a=1').toString() === 'a=1' && typeof URLSearchParams.prototype.set === 'function' && typeof URLSearchParams.prototype.entries === 'function';
} catch (e) {
return false;
}
};
if (!checkIfURLSearchParamsSupported()) {
polyfillURLSearchParams();
}
var proto = global.URLSearchParams.prototype;
if (typeof proto.sort !== 'function') {
proto.sort = function () {
var _this = this;
var items = [];
this.forEach(function (value, name) {
items.push([name, value]);
if (!_this._entries) {
_this.delete(name);
}
});
items.sort(function (a, b) {
if (a[0] < b[0]) {
return -1;
} else if (a[0] > b[0]) {
return +1;
} else {
return 0;
}
});
if (_this._entries) {
// force reset because IE keeps keys index
_this._entries = {};
}
for (var i = 0; i < items.length; i++) {
this.append(items[i][0], items[i][1]);
}
};
}
if (typeof proto._fromString !== 'function') {
Object.defineProperty(proto, '_fromString', {
enumerable: false,
configurable: false,
writable: false,
value: function (searchString) {
if (this._entries) {
this._entries = {};
} else {
var keys = [];
this.forEach(function (value, name) {
keys.push(name);
});
for (var i = 0; i < keys.length; i++) {
this.delete(keys[i]);
}
}
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.slice(1).join('=')) : '');
}
}
});
}
// 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 global.URL('b', 'http://a');
u.pathname = 'c d';
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);
if (base && typeof base !== 'string') base = String(base);
// Only create another document if the base is different from current location.
var doc = document,
baseElement;
if (base && (global.location === void 0 || base !== global.location.href)) {
base = base.toLowerCase();
doc = document.implementation.createHTMLDocument('');
baseElement = doc.createElement('base');
baseElement.href = base;
doc.head.appendChild(baseElement);
try {
if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);
} catch (err) {
throw new Error('URL unable to set base ' + base + ' due to ' + err);
}
}
var anchorElement = doc.createElement('a');
anchorElement.href = url;
if (baseElement) {
doc.body.appendChild(anchorElement);
anchorElement.href = anchorElement.href; // force href to refresh
}
var inputElement = doc.createElement('input');
inputElement.type = 'url';
inputElement.value = url;
if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || !inputElement.checkValidity() && !base) {
throw new TypeError('Invalid URL');
}
Object.defineProperty(this, '_anchorElement', {
value: anchorElement
});
// create a linked searchParams which reflect its changes on URL
var searchParams = new global.URLSearchParams(this.search);
var enableSearchUpdate = true;
var enableSearchParamsUpdate = true;
var _this = this;
['append', 'delete', 'set'].forEach(function (methodName) {
var method = searchParams[methodName];
searchParams[methodName] = function () {
method.apply(searchParams, arguments);
if (enableSearchUpdate) {
enableSearchParamsUpdate = false;
_this.search = searchParams.toString();
enableSearchParamsUpdate = true;
}
};
});
Object.defineProperty(this, 'searchParams', {
value: searchParams,
enumerable: true
});
var search = void 0;
Object.defineProperty(this, '_updateSearchParams', {
enumerable: false,
configurable: false,
writable: false,
value: function () {
if (this.search !== search) {
search = this.search;
if (enableSearchParamsUpdate) {
enableSearchUpdate = false;
this.searchParams._fromString(this.search);
enableSearchUpdate = true;
}
}
}
});
};
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'].forEach(function (attributeName) {
linkURLWithAnchorAttribute(attributeName);
});
Object.defineProperty(proto, 'search', {
get: function () {
return this._anchorElement['search'];
},
set: function (value) {
this._anchorElement['search'] = value;
this._updateSearchParams();
},
enumerable: true
});
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;
this._updateSearchParams();
},
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
}
});
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);
function _defineProperty$1(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _classCallCheck(e, t) {
if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, t) {
for (var n = 0; n < t.length; n++) {
var r = t[n];
r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);
}
}
function _createClass(e, t, n) {
return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;
}
function _defineProperty(e, t, n) {
return t in e ? Object.defineProperty(e, t, {
value: n,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[t] = n, e;
}
function ownKeys(e, t) {
var n = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var r = Object.getOwnPropertySymbols(e);
t && (r = r.filter(function (t) {
return Object.getOwnPropertyDescriptor(e, t).enumerable;
})), n.push.apply(n, r);
}
return n;
}
function _objectSpread2(e) {
for (var t = 1; t < arguments.length; t++) {
var n = null != arguments[t] ? arguments[t] : {};
t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {
_defineProperty(e, t, n[t]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {
Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));
});
}
return e;
}
var defaults$1 = {
addCSS: !0,
thumbWidth: 15,
watch: !0
};
function matches$1(e, t) {
return function () {
return Array.from(document.querySelectorAll(t)).includes(this);
}.call(e, t);
}
function trigger(e, t) {
if (e && t) {
var n = new Event(t, {
bubbles: !0
});
e.dispatchEvent(n);
}
}
var getConstructor$1 = function (e) {
return null != e ? e.constructor : null;
},
instanceOf$1 = function (e, t) {
return !!(e && t && e instanceof t);
},
isNullOrUndefined$1 = function (e) {
return null == e;
},
isObject$1 = function (e) {
return getConstructor$1(e) === Object;
},
isNumber$1 = function (e) {
return getConstructor$1(e) === Number && !Number.isNaN(e);
},
isString$1 = function (e) {
return getConstructor$1(e) === String;
},
isBoolean$1 = function (e) {
return getConstructor$1(e) === Boolean;
},
isFunction$1 = function (e) {
return getConstructor$1(e) === Function;
},
isArray$1 = function (e) {
return Array.isArray(e);
},
isNodeList$1 = function (e) {
return instanceOf$1(e, NodeList);
},
isElement$1 = function (e) {
return instanceOf$1(e, Element);
},
isEvent$1 = function (e) {
return instanceOf$1(e, Event);
},
isEmpty$1 = function (e) {
return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;
},
is$1 = {
nullOrUndefined: isNullOrUndefined$1,
object: isObject$1,
number: isNumber$1,
string: isString$1,
boolean: isBoolean$1,
function: isFunction$1,
array: isArray$1,
nodeList: isNodeList$1,
element: isElement$1,
event: isEvent$1,
empty: isEmpty$1
};
function getDecimalPlaces(e) {
var t = "".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;
}
function round(e, t) {
if (1 > t) {
var n = getDecimalPlaces(t);
return parseFloat(e.toFixed(n));
}
return Math.round(e / t) * t;
}
var RangeTouch = function () {
function e(t, n) {
_classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());
}
return _createClass(e, [{
key: "init",
value: function () {
e.enabled && (this.config.addCSS && (this.element.style.userSelect = "none", this.element.style.webKitUserSelect = "none", this.element.style.touchAction = "manipulation"), this.listeners(!0), this.element.rangeTouch = this);
}
}, {
key: "destroy",
value: function () {
e.enabled && (this.config.addCSS && (this.element.style.userSelect = "", this.element.style.webKitUserSelect = "", this.element.style.touchAction = ""), this.listeners(!1), this.element.rangeTouch = null);
}
}, {
key: "listeners",
value: function (e) {
var t = this,
n = e ? "addEventListener" : "removeEventListener";
["touchstart", "touchmove", "touchend"].forEach(function (e) {
t.element[n](e, function (e) {
return t.set(e);
}, !1);
});
}
}, {
key: "get",
value: function (t) {
if (!e.enabled || !is$1.event(t)) return null;
var n,
r = t.target,
i = t.changedTouches[0],
o = parseFloat(r.getAttribute("min")) || 0,
s = parseFloat(r.getAttribute("max")) || 100,
u = parseFloat(r.getAttribute("step")) || 1,
c = r.getBoundingClientRect(),
a = 100 / c.width * (this.config.thumbWidth / 2) / 100;
return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);
}
}, {
key: "set",
value: function (t) {
e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, "touchend" === t.type ? "change" : "input"));
}
}], [{
key: "setup",
value: function (t) {
var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},
r = null;
if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type="range"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;
var i = _objectSpread2({}, defaults$1, {}, n);
if (is$1.string(t) && i.watch) {
var o = new MutationObserver(function (n) {
Array.from(n).forEach(function (n) {
Array.from(n.addedNodes).forEach(function (n) {
is$1.element(n) && matches$1(n, t) && new e(n, i);
});
});
});
o.observe(document.body, {
childList: !0,
subtree: !0
});
}
return r.map(function (t) {
return new e(t, n);
});
}
}, {
key: "enabled",
get: function () {
return "ontouchstart" in document.documentElement;
}
}]), e;
}();
// ==========================================================================
// Type checking utils
// ==========================================================================
const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;
const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
const isNullOrUndefined = input => input === null || typeof input === 'undefined';
const isObject = input => getConstructor(input) === Object;
const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
const isString = input => getConstructor(input) === String;
const isBoolean = input => getConstructor(input) === Boolean;
const isFunction = input => typeof input === 'function';
const isArray = input => Array.isArray(input);
const isWeakMap = input => instanceOf(input, WeakMap);
const isNodeList = input => instanceOf(input, NodeList);
const isTextNode = input => getConstructor(input) === Text;
const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);
const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';
const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
const isUrl = input => {
// 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
let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`;
}
try {
return !isEmpty(new URL(string).hostname);
} catch (_) {
return false;
}
};
var is = {
nullOrUndefined: isNullOrUndefined,
object: isObject,
number: isNumber,
string: isString,
boolean: isBoolean,
function: isFunction,
array: isArray,
weakMap: isWeakMap,
nodeList: isNodeList,
element: isElement,
textNode: isTextNode,
event: isEvent,
keyboardEvent: isKeyboardEvent,
cue: isCue,
track: isTrack,
promise: isPromise,
url: isUrl,
empty: isEmpty
};
// ==========================================================================
// Animation utils
// ==========================================================================
const transitionEndEvent = (() => {
const element = document.createElement('span');
const events = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false;
})();
// Force repaint of element
function repaint(element, delay) {
setTimeout(() => {
try {
// eslint-disable-next-line no-param-reassign
element.hidden = true;
// eslint-disable-next-line no-unused-expressions
element.offsetHeight;
// eslint-disable-next-line no-param-reassign
element.hidden = false;
} catch (_) {
// Do nothing
}
}, delay);
}
// ==========================================================================
// Browser sniffing
// Unfortunately, due to mixed support, UA sniffing is required
// ==========================================================================
const isIE = Boolean(window.document.documentMode);
const isEdge = /Edge/g.test(navigator.userAgent);
const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
// navigator.platform may be deprecated but this check is still required
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
var browser = {
isIE,
isEdge,
isWebKit,
isIPhone,
isIPadOS,
isIos
};
// ==========================================================================
// Object utils
// ==========================================================================
// Clone nested objects
function cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
}
// Get a nested value in an object
function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object);
}
// Deep extend destination object with N more objects
function extend(target = {}, ...sources) {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, {
[key]: {}
});
}
extend(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
});
return extend(target, ...sources);
}
// ==========================================================================
// Element utils
// ==========================================================================
// Wrap an element
function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
Array.from(targets).reverse().forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling.
const parent = element.parentNode;
const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(element);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
});
}
// Set attributes
function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) return;
// Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));
}
// Create a DocumentFragment
function createElement(type, attributes, text) {
// Create a new <element>
const element = document.createElement(type);
// Set all passed attributes
if (is.object(attributes)) {
setAttributes(element, attributes);
}
// Add text node
if (is.string(text)) {
element.innerText = text;
}
// Return built element
return element;
}
// Insert an element after another
function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) return;
target.parentNode.insertBefore(element, target.nextSibling);
}
// Insert a DocumentFragment
function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) return;
parent.appendChild(createElement(type, attributes, text));
}
// Remove element(s)
function removeElement(element) {
if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement);
return;
}
if (!is.element(element) || !is.element(element.parentNode)) {
return;
}
element.parentNode.removeChild(element);
}
// Remove all child elements
function emptyElement(element) {
if (!is.element(element)) return;
let {
length
} = element.childNodes;
while (length > 0) {
element.removeChild(element.lastChild);
length -= 1;
}
}
// Replace element
function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
// Get an attribute object from a string selector
function getAttributesFromSelector(sel, existingAttributes) {
// For example:
// '.test' to { class: 'test' }
// '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) return {};
const attributes = {};
const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => {
// Remove whitespace
const selector = s.trim();
const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value
const parts = stripped.split('=');
const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character
const start = selector.charAt(0);
switch (start) {
case '.':
// Add to existing classname
if (is.string(existing.class)) {
attributes.class = `${existing.class} ${className}`;
} else {
attributes.class = className;
}
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
}
});
return extend(existing, attributes);
}
// Toggle hidden
function toggleHidden(element, hidden) {
if (!is.element(element)) return;
let hide = hidden;
if (!is.boolean(hide)) {
hide = !element.hidden;
}
// eslint-disable-next-line no-param-reassign
element.hidden = hide;
}
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
function toggleClass(element, className, force) {
if (is.nodeList(element)) {
return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[method](className);
return element.classList.contains(className);
}
return false;
}
// Has class name
function hasClass(element, className) {
return is.element(element) && element.classList.contains(className);
}
// Element matches selector
function matches(element, selector) {
const {
prototype
} = Element;
function match() {
return Array.from(document.querySelectorAll(selector)).includes(this);
}
const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
return method.call(element, selector);
}
// Closest ancestor element matching selector (also tests element itself)
function closest$1(element, selector) {
const {
prototype
} = Element;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() {
let el = this;
do {
if (matches.matches(el, selector)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
}
const method = prototype.closest || closestElement;
return method.call(element, selector);
}
// Find all elements
function getElements(selector) {
return this.elements.container.querySelectorAll(selector);
}
// Find a single element
function getElement(selector) {
return this.elements.container.querySelector(selector);
}
// Set focus and tab focus class
function setFocus(element = null, focusVisible = false) {
if (!is.element(element)) return;
// Set regular focus
element.focus({
preventScroll: true,
focusVisible
});
}
// ==========================================================================
// Plyr support checks
// ==========================================================================
// Default codecs for checking mimetype support
const defaultCodecs = {
'audio/ogg': 'vorbis',
'audio/wav': '1',
'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora'
};
// Check for feature support
const support = {
// Basic support
audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'),
// Check for support
// Basic functionality vs full UI
check(type, provider) {
const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput;
return {
api,
ui
};
},
// Picture-in-picture support
pip: (() => {
return document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture;
})(),
// Airplay support
// Safari only currently
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'),
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
mime(input) {
if (is.empty(input)) {
return false;
}
const [mediaType] = input.split('/');
let type = input;
// Verify we're using HTML5 and there's no media type mismatch
if (!this.isHTML5 || mediaType !== this.type) {
return false;
}
// Add codec if required
if (Object.keys(defaultCodecs).includes(type)) {
type += `; codecs="${defaultCodecs[input]}"`;
}
try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (_) {
return false;
}
},
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
// <input type="range"> Sliders
rangeInput: (() => {
const range = document.createElement('input');
range.type = 'range';
return range.type === 'range';
})(),
// Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
// Detect transitions support
transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
};
// ==========================================================================
// Event utils
// ==========================================================================
// Check for passive event listener support
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed
let supported = false;
try {
const options = Object.defineProperty({}, 'passive', {
get() {
supported = true;
return null;
}
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (_) {
// Do nothing
}
return supported;
})();
// Toggle event listener
function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return;
}
// Allow multiple events
const events = event.split(' ');
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({
element,
type,
callback,
options
});
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
}
// Bind event handler
function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture);
}
// Unbind event handler
function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture);
}
// Bind once-only event handler
function once(element, events = '', callback, passive = true, capture = false) {
const onceCallback = (...args) => {
off(element, events, onceCallback, passive, capture);
callback.apply(this, args);
};
toggleListener.call(this, element, events, onceCallback, true, passive, capture);
}
// Trigger event
function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element
if (!is.element(element) || is.empty(type)) {
return;
}
// Create and dispatch the event
const event = new CustomEvent(type, {
bubbles,
detail: {
...detail,
plyr: this
}
});
// Dispatch the event
element.dispatchEvent(event);
}
// Unbind all cached event listeners
function unbindListeners() {
if (this && this.eventListeners) {
this.eventListeners.forEach(item => {
const {
element,
type,
callback,
options
} = item;
element.removeEventListener(type, callback, options);
});
this.eventListeners = [];
}
}
// Run method when / if player is ready
function ready() {
return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});
}
/**
* Silence a Promise-like object.
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
* @param {Object} value An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (is.promise(value)) {
value.then(null, () => {});
}
}
// ==========================================================================
// Array utils
// ==========================================================================
// Remove duplicates in an array
function dedupe(array) {
if (!is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
}
// Get the closest value in an array
function closest(array, value) {
if (!is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
}
// ==========================================================================
// Style utils
// ==========================================================================
// Check support for a CSS declaration
function supportsCSS(declaration) {
if (!window || !window.CSS) {
return false;
}
return window.CSS.supports(declaration);
}
// Standard/common aspect ratios
const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({
...out,
[x / y]: [x, y]
}), {});
// Validate an aspect ratio
function validateAspectRatio(input) {
if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
return false;
}
const ratio = is.array(input) ? input : input.split(':');
return ratio.map(Number).every(is.number);
}
// Reduce an aspect ratio to it's lowest form
function reduceAspectRatio(ratio) {
if (!is.array(ratio) || !ratio.every(is.number)) {
return null;
}
const [width, height] = ratio;
const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);
const divider = getDivider(width, height);
return [width / divider, height / divider];
}
// Calculate an aspect ratio
function getAspectRatio(input) {
const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;
// Try provided ratio
let ratio = parse(input);
// Get from config
if (ratio === null) {
ratio = parse(this.config.ratio);
}
// Get from embed
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
({
ratio
} = this.embed);
}
// Get from HTML5 video
if (ratio === null && this.isHTML5) {
const {
videoWidth,
videoHeight
} = this.media;
ratio = [videoWidth, videoHeight];
}
return reduceAspectRatio(ratio);
}
// Set aspect ratio for responsive container
function setAspectRatio(input) {
if (!this.isVideo) {
return {};
}
const {
wrapper
} = this.elements;
const ratio = getAspectRatio.call(this, input);
if (!is.array(ratio)) {
return {};
}
const [x, y] = reduceAspectRatio(ratio);
const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);
const padding = 100 / x * y;
if (useNative) {
wrapper.style.aspectRatio = `${x}/${y}`;
} else {
wrapper.style.paddingBottom = `${padding}%`;
}
// For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
const offset = (height - padding) / (height / 50);
if (this.fullscreen.active) {
wrapper.style.paddingBottom = null;
} else {
this.media.style.transform = `translateY(-${offset}%)`;
}
} else if (this.isHTML5) {
wrapper.classList.add(this.config.classNames.videoFixedRatio);
}
return {
padding,
ratio
};
}
// Round an aspect ratio to closest standard ratio
function roundAspectRatio(x, y, tolerance = 0.05) {
const ratio = x / y;
const closestRatio = closest(Object.keys(standardRatios), ratio);
// Check match is within tolerance
if (Math.abs(closestRatio - ratio) <= tolerance) {
return standardRatios[closestRatio];
}
// No match
return [x, y];
}
// Get the size of the viewport
// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions
function getViewportSize() {
const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
return [width, height];
}
// ==========================================================================
// Plyr HTML5 helpers
// ==========================================================================
const html5 = {
getSources() {
if (!this.isHTML5) {
return [];
}
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
return sources.filter(source => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);
},