Compare commits

...

77 Commits

Author SHA1 Message Date
Sam Potts d70a787af1 Merge branch 'develop' 2018-05-28 10:42:11 +10:00
Sam Potts 69bb0917ad v3.3.9 2018-05-28 10:41:51 +10:00
Sam Potts 6f256d09b2 ESLint tweak 2018-05-28 10:18:04 +10:00
Sam Potts e9684c2021 Merge pull request #979 from friday/respect-storage
Respect storage being disabled for storage getter
2018-05-28 10:09:02 +10:00
Sam Potts bf91a0e73f Merge pull request #977 from friday/utils.is.icue-ie11
Restore utils.is.cue()
2018-05-28 10:08:42 +10:00
Sam Potts 14b6309aef Merge pull request #978 from friday/ie-issues
Fix InvalidStateError and IE11 issues
2018-05-28 10:08:24 +10:00
Albin Larsson 6391ced99f If storage is disabled, disable get as well, not just set 2018-05-28 01:58:06 +02:00
Albin Larsson fac8a185ba Simplify currentTime setter and bail when media hasn't loaded 2018-05-28 00:57:07 +02:00
Albin Larsson c69aa8a42b Avoid duration getter returning NaN before element has loaded 2018-05-28 00:57:01 +02:00
Albin Larsson f34bf22125 Restore utils.is.cue() 2018-05-27 20:28:48 +02:00
Sam Potts f0be913dc3 Merge pull request #975 from sampotts/develop
v3.3.8
2018-05-26 13:55:22 +10:00
Sam Potts cd51788b98 v3.3.8 2018-05-26 13:53:15 +10:00
Sam Potts edd67b0da3 Typo 2018-05-20 23:44:40 +10:00
Sam Potts d733454d7f Pause while seeking 2018-05-20 23:40:28 +10:00
Sam Potts 41f9a87e0e Add URL polyfill 2018-05-20 23:40:00 +10:00
Sam Potts f4858f0c62 Merge pull request #959 from friday/876
Youtube and vimeo fixes
2018-05-19 16:49:31 +10:00
Albin Larsson 121093ae71 Prevent durationchange events from showing time when invertTime is false 2018-05-19 04:27:45 +02:00
Albin Larsson aa8fc313a9 Fix #966: Add 'seeked' event listener to update progress (seeking doesn't have the correct time) 2018-05-19 04:23:22 +02:00
Albin Larsson 723298a07b Fix #921: Trigger seeked event in youtube plugin if either playing or paused 2018-05-19 04:18:27 +02:00
Albin Larsson f8c89e3e95 Fix #876: YouTube and Vimeo autoplays on seek 2018-05-19 04:18:27 +02:00
Albin Larsson 333435a9c2 Fix playback state (paused) and events (play/pause) 2018-05-19 04:18:27 +02:00
Sam Potts 3ab2295fe7 Merge branch 'master' into develop 2018-05-19 11:29:47 +10:00
Sam Potts c41bb657ac Merge pull request #958 from friday/954
Fix the seek tooltip time difference from seek time
2018-05-19 11:28:38 +10:00
Sam Potts 55bbf64f2b Merge pull request #963 from friday/verify-poster
Make sure poster element isn't shown if the image isn't loaded
2018-05-19 11:27:52 +10:00
Sam Potts 3bba65f2c2 Merge pull request #967 from friday/883
toggleControls rewrite
2018-05-19 11:27:19 +10:00
Sam Potts 1bab0d07b5 Merge branch 'master' into develop 2018-05-19 11:26:05 +10:00
Sam Potts 602353f4d9 Merge branch 'master' of github.com:sampotts/plyr 2018-05-19 11:25:02 +10:00
Sam Potts 51814249af Reduce circular dependencies 2018-05-19 11:24:56 +10:00
Albin Larsson 37c5fbfe16 toggleControls() rewrite 2018-05-18 16:19:38 +02:00
Albin Larsson d7356726a1 Remove ui.checkFailed() and error class 2018-05-16 23:21:06 +02:00
Albin Larsson 4db6bf7a2e Make utils.toggleClass() compatible with Element.classList.toggle (rename toggle argument to 'force' and make it optional) 2018-05-16 23:21:06 +02:00
Albin Larsson 28826f6402 Add 'video only' caveat to toggleControls() doc (current behavior) 2018-05-16 23:21:06 +02:00
Albin Larsson c845558d96 Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails 2018-05-15 16:41:51 +02:00
Albin Larsson 16c3a7d9e5 Rewrite ui.setPoster to check that images arent broken or youtube fallback images. Only show poster element when valid 2018-05-15 16:21:36 +02:00
Albin Larsson 90d5b48845 Add async method to utils for loading/checking images 2018-05-15 13:27:55 +02:00
Albin Larsson d1acc4abb3 Add event before seeking via mouse interaction to set alternative 'value' for the input matching the tooltip time 2018-05-14 19:50:08 +02:00
Sam Potts 797b70998f Merge pull request #960 from friday/935
Support importing Plyr in Node.js without errors
2018-05-14 23:38:48 +10:00
Sam Potts 4a01027da0 Merge pull request #961 from friday/expose-defaults
Enable overriding defaults
2018-05-14 23:38:25 +10:00
Albin Larsson 7ca2169790 Expose defaults (enable overriding) 2018-05-14 06:49:04 +02:00
Albin Larsson 054f522aa9 Enable importing Plyr in node.js without errors (resulting in an empty object) 2018-05-14 05:35:13 +02:00
Albin Larsson f2fc3f5ea5 Fix the seek tooltip time difference from seek time 2018-05-12 00:10:39 +02:00
Sam Potts 765c01e83d Remove references to window.Plyr 2018-05-10 09:34:15 +10:00
Sam Potts 33a11fb53a v3.3.7 2018-05-09 09:50:22 +10:00
Sam Potts d1d41ca49a Merge branch 'master' of github.com:sampotts/plyr 2018-05-09 09:48:52 +10:00
Sam Potts c06e0ee5e9 Grid tweak 2018-05-09 09:48:46 +10:00
Sam Potts 83f80ccc40 Merge pull request #950 from friday/poster-fixes
Poster fixes
2018-05-09 09:44:36 +10:00
Albin Larsson 069065ea3a Fix #946 - poster getting click events 2018-05-08 16:50:40 +02:00
Albin Larsson 1672e78041 Fix poster being stretched 2018-05-08 16:49:32 +02:00
Sam Potts 34401de3d0 Merge branch 'master' into develop 2018-05-08 22:22:43 +10:00
Sam Potts f687b81b70 v3.3.6 2018-05-08 13:18:30 +10:00
Sam Potts bbb11e611e Vimeo options, docs for multiple players 2018-05-08 13:12:39 +10:00
Sam Potts 90919411e9 Use div for poster, Vimeo fixes, Tooltip fixes 2018-05-08 12:57:24 +10:00
Sam Potts 1491b017a0 Setup multiple players 2018-05-06 16:18:10 +10:00
Sam Potts 1655150092 v3.3.5 2018-05-06 01:32:51 +10:00
Sam Potts ceb6c9a100 v3.3.3 2018-05-06 01:16:41 +10:00
Sam Potts 00bbce08fb Reverted menu change 2018-05-06 01:14:41 +10:00
Sam Potts 91a4b86860 Small bug fixes 2018-05-06 01:03:38 +10:00
Sam Potts 5aece6fa06 Merge 2018-05-06 00:50:02 +10:00
Sam Potts a70b94afe2 Merge branch 'master' of github.com:sampotts/plyr 2018-05-06 00:49:22 +10:00
Sam Potts 9ebc2719d3 v3.3.0 2018-05-06 00:49:12 +10:00
Sam Potts b46aae1833 Merge pull request #939 from Billybobbonnet/patch-1
Added 480p to SD labels
2018-05-03 20:28:03 +10:00
Antoine Cordelois 30e6a40865 Added 480p to SD labels 2018-05-03 11:42:09 +02:00
Sam Potts 403df36af6 Merge branch 'master' into develop 2018-04-27 21:42:29 +10:00
Sam Potts 5ca769807e Merge pull request #923 from friday/922
Only add hideControls class if config.hideControls is truthy
2018-04-27 20:07:18 +10:00
Sam Potts 72a71a605b Fix for default timestamp 2018-04-27 20:06:14 +10:00
Sam Potts 24d833a5d1 Merge branch 'master' into develop 2018-04-27 18:35:06 +10:00
Sam Potts 44b30380f7 Merge branch 'beta' of github.com:Selz/plyr 2018-04-27 18:34:06 +10:00
Sam Potts 261cd086c7 Update readme.md 2018-04-27 12:44:58 +10:00
Albin Larsson 9e19b526b9 Only add hideControls class if config.hideControls is truthy 2018-04-26 17:51:14 +02:00
Sam Potts 6c617a0ef1 Readme fix 2018-04-27 01:12:04 +10:00
Sam Potts a812650fea v3.2.4 2018-04-27 00:47:51 +10:00
Sam Potts fec7a77d6f v3.2.3 2018-04-25 20:02:36 +10:00
Sam Potts 971e261067 Fix for iOS 9 throwing error for name property in fullscreen API (fixes #908) 2018-04-25 19:59:22 +10:00
Sam Potts 27407ba021 v3.2.2 2018-04-25 19:46:39 +10:00
Sam Potts ef8e58ede4 Fix for hidden buffer and incorrect use of aria-hidden 2018-04-25 19:40:23 +10:00
Sam Potts f13260c10a Merge pull request #919 from sampotts/master
Merge back
2018-04-25 07:38:17 +10:00
Sam Potts e1183d6049 Merge pull request #918 from sampotts/master
Merge back
2018-04-25 07:37:18 +10:00
50 changed files with 10573 additions and 7835 deletions
+1
View File
@@ -32,6 +32,7 @@
"message": "Use local parameter instead."
}
],
"no-param-reassign": [2, { "props": false }],
"array-bracket-newline": [2, { "minItems": 2 }],
"array-element-newline": [2, { "minItems": 2 }]
},
+81
View File
@@ -1,3 +1,84 @@
# v3.3.9
Again, more changes from @friday!
* Restore window reference in `utils.is.cue()`
* Fix InvalidStateError and IE11 issues
* Respect storage being disabled for storage getter
# v3.3.8
Many changes here thanks to @friday:
* Added missing URL polyfill
* Pause while seeking to mimic default HTML5 behaviour
* Add `seeked` event listener to update progress (fixes #966)
* Trigger seeked event in youtube plugin if either playing or paused (fixes #921)
* Fix for YouTube and Vimeo autoplays on seek (fixes #876)
* Toggle controls improvements
* Cleanup unused code
* Poster image loading improvements
* Fix for seek tooltip vs click accuracy
# v3.3.7
* Poster fixes (thanks @friday)
* Grid tweak
# v3.3.6
* Vimeo fixes for mute state
* Vimeo ID fix (fixes #945)
* Use `<div>` for poster container
* Tooltip fixes for unicode languages (fixes #943)
# v3.3.5
* Removed `.load()` call as it breaks HLS (see #870)
# v3.3.4
* Fix for controls sometimes not showing while video is playing
* Fixed logic for show home tab on option select
# v3.3.3
* Reverted change to show home tab on option select due to usability regression
# v3.3.2
* Fix for ads running in audio
* Fix for setting poster on source change
## v3.3.0
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
* Fix for same domain SVG URLs (raised by Jochem in Slack)
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
* Added pause className (fixes #941)
* Button height set in CSS (auto) (fixes #928)
* Don't autoplay cloned original media (fixes #936)
* Return to the home menu pane after selecting an option
## v3.2.4
* Fix issue wher player never reports as ready if controls is empty array
* Fix issue where screen reader labels were removed from time displays
* Fix issue where custom controls placeholders were not populated
* Custom controls HTML example updated
* Fix for aria-label being set to the initial state on toggle buttons, overriding the inner labels
* Fix for hidden mute button on iOS (not functional for Vimeo due to API limitations) (fixes #656)
## v3.2.3
* Fix for iOS 9 throwing error for `name` property in fullscreen API (fixes #908)
## v3.2.2
* Fix for regression in 3.2.1 resulting in hidden buffer display (fixes #920)
* Cleaned up incorrect use of `aria-hidden` attribute
## v3.2.1
* Accessibility improvements for the controls (part of #905 fixes)
+2 -1
View File
@@ -121,7 +121,8 @@ const controls = `
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
<span role="tooltip" class="plyr__tooltip">00:00</span>
</div>
<div class="plyr__time">00:00</div>
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
<div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
<button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
<svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
+1 -1
View File
File diff suppressed because one or more lines are too long
+200 -31
View File
@@ -1,4 +1,4 @@
(function () {
typeof navigator === "object" && (function () {
'use strict';
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -97,7 +97,7 @@ function isObject(what) {
// Yanked from https://git.io/vS8DV re-used under CC0
// with some tiny modifications
function isError(value) {
switch ({}.toString.call(value)) {
switch (Object.prototype.toString.call(value)) {
case '[object Error]':
return true;
case '[object Exception]':
@@ -110,7 +110,15 @@ function isError(value) {
}
function isErrorEvent(value) {
return supportsErrorEvent() && {}.toString.call(value) === '[object ErrorEvent]';
return Object.prototype.toString.call(value) === '[object ErrorEvent]';
}
function isDOMError(value) {
return Object.prototype.toString.call(value) === '[object DOMError]';
}
function isDOMException(value) {
return Object.prototype.toString.call(value) === '[object DOMException]';
}
function isUndefined(what) {
@@ -153,6 +161,24 @@ function supportsErrorEvent() {
}
}
function supportsDOMError() {
try {
new DOMError(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsDOMException() {
try {
new DOMException(''); // eslint-disable-line no-new
return true;
} catch (e) {
return false;
}
}
function supportsFetch() {
if (!('fetch' in _window)) return false;
@@ -245,7 +271,13 @@ function objectFrozen(obj) {
}
function truncate(str, max) {
return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
if (typeof max !== 'number') {
throw new Error('2nd argument to `truncate` function should be a number');
}
if (typeof str !== 'string' || max === 0) {
return str;
}
return str.length <= max ? str : str.substr(0, max) + '\u2026';
}
/**
@@ -544,10 +576,9 @@ function jsonSize(value) {
}
function serializeValue(value) {
var maxLength = 40;
if (typeof value === 'string') {
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
var maxLength = 40;
return truncate(value, maxLength);
} else if (
typeof value === 'number' ||
typeof value === 'boolean' ||
@@ -663,6 +694,8 @@ var utils = {
isObject: isObject,
isError: isError,
isErrorEvent: isErrorEvent,
isDOMError: isDOMError,
isDOMException: isDOMException,
isUndefined: isUndefined,
isFunction: isFunction,
isPlainObject: isPlainObject,
@@ -670,6 +703,8 @@ var utils = {
isArray: isArray,
isEmptyObject: isEmptyObject,
supportsErrorEvent: supportsErrorEvent,
supportsDOMError: supportsDOMError,
supportsDOMException: supportsDOMException,
supportsFetch: supportsFetch,
supportsReferrerPolicy: supportsReferrerPolicy,
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
@@ -724,10 +759,24 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
function getLocationHref() {
if (typeof document === 'undefined' || document.location == null) return '';
return document.location.href;
}
function getLocationOrigin() {
if (typeof document === 'undefined' || document.location == null) return '';
// Oh dear IE10...
if (!document.location.origin) {
document.location.origin =
document.location.protocol +
'//' +
document.location.hostname +
(document.location.port ? ':' + document.location.port : '');
}
return document.location.origin;
}
/**
* TraceKit.report: cross-browser processing of unhandled exceptions
*
@@ -1135,6 +1184,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
element.func = UNKNOWN_FUNCTION;
}
if (element.url && element.url.substr(0, 5) === 'blob:') {
// Special case for handling JavaScript loaded into a blob.
// We use a synchronous AJAX request here as a blob is already in
// memory - it's not making a network request. This will generate a warning
// in the browser console, but there has already been an error so that's not
// that much of an issue.
var xhr = new XMLHttpRequest();
xhr.open('GET', element.url, false);
xhr.send(null);
// If we failed to download the source, skip this patch
if (xhr.status === 200) {
var source = xhr.responseText || '';
// We trim the source down to the last 300 characters as sourceMappingURL is always at the end of the file.
// Why 300? To be in line with: https://github.com/getsentry/sentry/blob/4af29e8f2350e20c28a6933354e4f42437b4ba42/src/sentry/lang/javascript/processor.py#L164-L175
source = source.slice(-300);
// Now we dig out the source map URL
var sourceMaps = source.match(/\/\/# sourceMappingURL=(.*)$/);
// If we don't find a source map comment or we find more than one, continue on to the next element.
if (sourceMaps) {
var sourceMapAddress = sourceMaps[1];
// Now we check to see if it's a relative URL.
// If it is, convert it to an absolute one.
if (sourceMapAddress.charAt(0) === '~') {
sourceMapAddress = getLocationOrigin() + sourceMapAddress.slice(1);
}
// Now we strip the '.map' off of the end of the URL and update the
// element so that Sentry can match the map to the blob.
element.url = sourceMapAddress.slice(0, -4);
}
}
}
stack.push(element);
}
@@ -1646,10 +1733,12 @@ var console$1 = {
var isErrorEvent$1 = utils.isErrorEvent;
var isDOMError$1 = utils.isDOMError;
var isDOMException$1 = utils.isDOMException;
var isError$1 = utils.isError;
var isObject$1 = utils.isObject;
var isPlainObject$1 = utils.isPlainObject;
var isErrorEvent$1 = utils.isErrorEvent;
var isUndefined$1 = utils.isUndefined;
var isFunction$1 = utils.isFunction;
var isString$1 = utils.isString;
@@ -1777,7 +1866,7 @@ Raven.prototype = {
// webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465
VERSION: '3.24.0',
VERSION: '3.25.2',
debug: false,
@@ -2066,7 +2155,11 @@ Raven.prototype = {
*/
_promiseRejectionHandler: function(event) {
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
this.captureException(event.reason);
this.captureException(event.reason, {
extra: {
unhandledPromiseRejection: true
}
});
},
/**
@@ -2105,6 +2198,23 @@ Raven.prototype = {
if (isErrorEvent$1(ex) && ex.error) {
// If it is an ErrorEvent with `error` property, extract it to get actual Error
ex = ex.error;
} else if (isDOMError$1(ex) || isDOMException$1(ex)) {
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
// then we just extract the name and message, as they don't provide anything else
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
var name = ex.name || (isDOMError$1(ex) ? 'DOMError' : 'DOMException');
var message = ex.message ? name + ': ' + ex.message : name;
return this.captureMessage(
message,
objectMerge$1(options, {
// neither DOMError or DOMException provide stack trace and we most likely wont get it this way as well
// but it's barely any overhead so we may at least try
stacktrace: true,
trimHeadFrames: options.trimHeadFrames + 1
})
);
} else if (isError$1(ex)) {
// we have a real Error object
ex = ex;
@@ -2116,6 +2226,7 @@ Raven.prototype = {
ex = new Error(options.message);
} else {
// If none of previous checks were valid, then it means that
// it's not a DOMError/DOMException
// it's not a plain Object
// it's not a valid ErrorEvent (one with an error property)
// it's not an Error
@@ -2207,6 +2318,14 @@ Raven.prototype = {
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
var initialCall = isArray$1(stack.stack) && stack.stack[1];
// if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
// to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
// initialCall => captureException(string) => captureMessage(string)
if (initialCall && initialCall.func === 'Raven.captureException') {
initialCall = stack.stack[2];
}
var fileurl = (initialCall && initialCall.url) || '';
if (
@@ -3004,17 +3123,30 @@ Raven.prototype = {
status_code: null
};
return origFetch.apply(this, args).then(function(response) {
fetchData.status_code = response.status;
return origFetch
.apply(this, args)
.then(function(response) {
fetchData.status_code = response.status;
self.captureBreadcrumb({
type: 'http',
category: 'fetch',
data: fetchData
self.captureBreadcrumb({
type: 'http',
category: 'fetch',
data: fetchData
});
return response;
})
['catch'](function(err) {
// if there is an error performing the request
self.captureBreadcrumb({
type: 'http',
category: 'fetch',
data: fetchData,
level: 'error'
});
throw err;
});
return response;
});
};
},
wrappedBuiltIns
@@ -3027,7 +3159,7 @@ Raven.prototype = {
if (_document.addEventListener) {
_document.addEventListener('click', self._breadcrumbEventHandler('click'), false);
_document.addEventListener('keypress', self._keypressEventHandler(), false);
} else {
} else if (_document.attachEvent) {
// IE8 Compatibility
_document.attachEvent('onclick', self._breadcrumbEventHandler('click'));
_document.attachEvent('onkeypress', self._keypressEventHandler());
@@ -3043,8 +3175,8 @@ Raven.prototype = {
var hasPushAndReplaceState =
!isChromePackagedApp &&
_window$2.history &&
history.pushState &&
history.replaceState;
_window$2.history.pushState &&
_window$2.history.replaceState;
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
// TODO: remove onpopstate handler on uninstall()
var oldOnPopState = _window$2.onpopstate;
@@ -3073,8 +3205,8 @@ Raven.prototype = {
};
};
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
}
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
@@ -3290,7 +3422,7 @@ Raven.prototype = {
}
]
},
culprit: fileurl
transaction: fileurl
},
options
);
@@ -3364,7 +3496,7 @@ Raven.prototype = {
if (this._hasNavigator && _navigator.userAgent) {
httpData.headers = {
'User-Agent': navigator.userAgent
'User-Agent': _navigator.userAgent
};
}
@@ -3405,7 +3537,7 @@ Raven.prototype = {
if (
!last ||
current.message !== last.message || // defined for captureMessage
current.culprit !== last.culprit // defined for captureException/onerror
current.transaction !== last.transaction // defined for captureException/onerror
)
return false;
@@ -3750,7 +3882,11 @@ Raven.prototype = {
},
_logDebug: function(level) {
if (this._originalConsoleMethods[level] && this.debug) {
// We allow `Raven.debug` and `Raven.config(DSN, { debug: true })` to not make backward incompatible API change
if (
this._originalConsoleMethods[level] &&
(this.debug || this._globalOptions.debug)
) {
// In IE<10 console methods do not have their own 'apply' method
Function.prototype.apply.call(
this._originalConsoleMethods[level],
@@ -3823,11 +3959,11 @@ var singleton = Raven$1;
* const someAppReporter = new Raven.Client();
* const someOtherAppReporter = new Raven.Client();
*
* someAppReporter('__DSN__', {
* someAppReporter.config('__DSN__', {
* ...config goes here
* });
*
* someOtherAppReporter('__OTHER_DSN__', {
* someOtherAppReporter.config('__OTHER_DSN__', {
* ...config goes here
* });
*
@@ -3914,6 +4050,39 @@ singleton.Client = Client;
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: {
active: true
},
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -171,7 +171,7 @@
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values"
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script>
<!-- Plyr core script -->
+33
View File
@@ -74,6 +74,39 @@ import Raven from 'raven-js';
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: {
active: true,
},
+1
View File
@@ -29,6 +29,7 @@ video {
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player
+1 -1
View File
@@ -2,4 +2,4 @@
// Layout
// ==========================================================================
$container-max-width: 1280px;
$container-max-width: 1260px;
+1 -1
View File
File diff suppressed because one or more lines are too long
+3669 -3456
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4058 -3473
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+12 -9
View File
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css');
const run = require('run-sequence');
const header = require('gulp-header');
const prefix = require('gulp-autoprefixer');
const gitbranch = require('git-branch');
const svgstore = require('gulp-svgstore');
@@ -129,7 +130,7 @@ const build = {
tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
@@ -146,6 +147,7 @@ const build = {
options,
),
)
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
@@ -162,7 +164,7 @@ const build = {
const name = `sass:${key}`;
tasks.sass.push(name);
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
@@ -180,7 +182,7 @@ const build = {
tasks.sprite.push(name);
// Process Icons
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
.pipe(
@@ -239,11 +241,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
const branch = {
current: gitbranch.sync(),
master: 'master',
beta: 'beta',
develop: 'develop',
};
const allowed = [
branch.master,
branch.beta,
branch.develop,
];
const maxAge = 31536000; // 1 year
@@ -255,7 +257,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
},
},
demo: {
uploadPath: branch.current === branch.beta ? 'beta/' : null,
uploadPath: branch.current === branch.develop ? 'beta/' : null,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
Vary: 'Accept-Encoding',
@@ -287,7 +289,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
'plyr.polyfilled.js',
'defaults.js',
];
gulp
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
@@ -406,7 +409,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
});
// Do everything
gulp.task('publish', () => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
});
}
+17 -13
View File
@@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.2.1",
"version": "3.3.9",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist/plyr.js",
@@ -8,45 +8,48 @@
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
"babel-preset-env": "^1.7.0",
"del": "^3.0.0",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-import": "^2.12.0",
"git-branch": "^2.0.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-header": "^2.0.5",
"gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1",
"gulp-postcss": "^7.0.1",
"gulp-rename": "^1.2.3",
"gulp-replace": "^1.0.0",
"gulp-s3": "^0.11.0",
"gulp-sass": "^4.0.1",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1",
"gulp-uglify-es": "^1.0.4",
"gulp-util": "^3.0.8",
"postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.2.0",
"stylelint": "^9.2.1",
"stylelint-config-prettier": "^3.2.0",
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^3.0.0",
"stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
@@ -69,6 +72,7 @@
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4",
"raven-js": "^3.24.0"
"raven-js": "^3.25.2",
"url-polyfill": "^1.0.13"
}
}
+61 -35
View File
@@ -39,13 +39,13 @@ Check out the [changelog](changelog.md) to see what's new with Plyr.
Some awesome folks have made plugins for CMSs and Components for JavaScript frameworks:
| Type | Maintainer | Link |
| --------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Ryan Anthony Drake ([@iamryandrake](https://github.com/iamryandrake)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
| Type | Maintainer | Link |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| WordPress | Brandon Lavigne ([@drrobotnik](https://github.com/drrobotnik)) | [https://wordpress.org/plugins/plyr/](https://wordpress.org/plugins/plyr/) |
| React | Jose Miguel Bejarano ([@xDae](https://github.com/xDae)) | [https://github.com/xDae/react-plyr](https://github.com/xDae/react-plyr) |
| Vue | Gabe Dunn ([@redxtech](https://github.com/redxtech)) | [https://github.com/redxtech/vue-plyr](https://github.com/redxtech/vue-plyr) |
| Neos | Jon Uhlmann ([@jonnitto](https://github.com/jonnitto)) | [https://packagist.org/packages/jonnitto/plyr](https://packagist.org/packages/jonnitto/plyr) |
| Kirby | Dominik Pschenitschni ([@dpschen](https://github.com/dpschen)) | [https://github.com/dpschen/kirby-plyrtag](https://github.com/dpschen/kirby-plyrtag) |
## Quick setup
@@ -125,13 +125,17 @@ Include the `plyr.js` script before the closing `</body>` tag and then in your J
See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
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.2.1/plyr.js"></script>
<script src="https://cdn.plyr.io/3.3.9/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
...or...
```html
<script src="https://cdn.plyr.io/3.3.9/plyr.polyfilled.js"></script>
```
### CSS
@@ -144,13 +148,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.2.1/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.9/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.2.1/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.9/plyr.svg`.
## Ads
@@ -210,7 +214,7 @@ You can specify a range of arguments for the constructor to use:
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
Here's some examples
@@ -226,20 +230,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
const player = new Plyr(document.getElementById('player'));
```
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
```javascript
const player = new Plyr(document.querySelectorAll('.js-player'));
```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
##### Setting up multiple players
You have two choices here. You can either use a simple array loop to map the constructor:
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
```javascript
const players = Plyr.setup('.js-player');
```
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
##### Passing options
The second argument for the constructor is the [options](#options) object:
```javascript
@@ -248,7 +264,7 @@ const player = new Plyr('#player', {
});
```
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
#### Options
@@ -279,7 +295,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
@@ -345,7 +361,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `fullscreen.exit()` | - | Exit fullscreen. |
| `fullscreen.toggle()` | - | Toggle fullscreen. |
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
| `supports(type)` | String | Check support for a mime type. |
@@ -374,8 +390,9 @@ player.fullscreen.active; // false;
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
@@ -388,7 +405,7 @@ player.fullscreen.active; // false;
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `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. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
@@ -599,6 +616,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
_Note_: These need updating to use the new v3 syntax but would still work.
## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
@@ -607,19 +626,20 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | --------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓ |
| IE10 | ✓&sup2; |
| Browser | Supported |
| ------------- | ------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills
@@ -668,8 +688,10 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
## Donate
Plyr costs money to run, not only my time - I donate that for free but domains, hosting and more. Any help is appreciated...
[Donate to support Plyr](https://www.paypal.me/pottsy/20usd)
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
* [Donate via Patron](https://www.patreon.com/plyr)
* [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
## Mentions
@@ -707,10 +729,14 @@ Credit to the PayPal HTML5 Video player from which Plyr's caption functionality
## Thanks
[![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/)
[![Fastly](https://cdn.plyr.io/static/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
[![Sentry](https://cdn.plyr.io/static/sentry-logo-black.svg)](https://sentry.io/)
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
## Copyright and License
[The MIT license](license.md)
+3 -3
View File
@@ -3,10 +3,10 @@
// TODO: Create as class
// ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
const captions = {
// Setup captions
@@ -236,7 +236,7 @@ const captions = {
// Set the span content
if (utils.is.string(caption)) {
content.textContent = caption.trim();
content.innerText = caption.trim();
} else {
content.appendChild(caption);
}
+284 -81
View File
@@ -2,54 +2,88 @@
// Plyr controls
// ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions';
import html5 from './html5';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const controls = {
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
// Needs to be a valid <input type='range'>
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
},
// Get icon URL
getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
return {
url: this.config.iconUrl,
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody),
cors,
};
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Create <svg> icon
createIcon(type, attributes) {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`;
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
@@ -212,7 +246,6 @@ const controls = {
// Add aria attributes
attributes['aria-pressed'] = false;
attributes['aria-label'] = i18n.get(label, this.config);
} else {
button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label));
@@ -319,7 +352,7 @@ const controls = {
break;
}
progress.textContent = `% ${suffix.toLowerCase()}`;
progress.innerText = `% ${suffix.toLowerCase()}`;
}
this.elements.display[type] = progress;
@@ -329,22 +362,14 @@ const controls = {
// Create time display
createTime(type) {
const container = utils.createElement('div', {
class: 'plyr__time',
});
const attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
container.appendChild(
utils.createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get(type, this.config),
),
);
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
const container = utils.createElement('div', utils.extend(attributes, {
class: `plyr__time ${attributes.class}`,
'aria-label': i18n.get(type, this.config),
}), '00:00');
// Reference for updates
this.elements.display[type] = container;
return container;
@@ -369,7 +394,7 @@ const controls = {
}),
);
const faux = utils.createElement('span', { 'aria-hidden': true });
const faux = utils.createElement('span', { hidden: '' });
label.appendChild(radio);
label.appendChild(faux);
@@ -383,6 +408,124 @@ const controls = {
list.appendChild(item);
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
return;
}
// Always display hours if duration is over an hour
const forceHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.innerText = utils.formatTime(time, forceHours, inverted);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
const setProgress = (target, input) => {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
};
if (event) {
switch (event.type) {
// 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
if (event.type === 'timeupdate') {
controls.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
setProgress(this.elements.display.buffer, this.buffered * 100);
break;
default:
break;
}
}
},
// Webkit polyfill for lower fill range
updateRangeFill(target) {
// Get range from event if event passed
const range = utils.is.event(target) ? target.target : target;
// Needs to be a valid <input type='range'>
if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
return;
}
// Set aria value for https://github.com/sampotts/plyr/issues/905
range.setAttribute('aria-valuenow', range.value);
// WebKit only
if (!browser.isWebkit) {
return;
}
// Set CSS custom property
range.style.setProperty('--value', `${range.value / range.max * 100}%`);
},
// Update hover tooltip for seeking
updateSeekTooltip(event) {
// Bail if setting not true
@@ -397,7 +540,7 @@ const controls = {
// Calculate percentage
let percent = 0;
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
const clientRect = this.elements.progress.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
@@ -427,7 +570,7 @@ const controls = {
}
// Display the time a click would seek to
ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
// Set position
this.elements.display.seekTooltip.style.left = `${percent}%`;
@@ -442,13 +585,50 @@ const controls = {
}
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
controls.updateProgress.call(this, event);
},
// Show the duration on metadataloaded or durationchange events
durationUpdate() {
// 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;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
// Hide/show a tab
toggleTab(setting, toggle) {
const tab = this.elements.settings.tabs[setting];
const pane = this.elements.settings.panes[setting];
utils.toggleHidden(tab, !toggle);
utils.toggleHidden(pane, !toggle);
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
},
// Set the quality menu
@@ -498,6 +678,7 @@ const controls = {
break;
case 576:
case 480:
label = 'SD';
break;
@@ -660,7 +841,6 @@ const controls = {
// Get current selected caption language
// TODO: rework this to user the getter in the API?
// Set a list of available captions languages
setCaptionsMenu() {
// TODO: Captions or language? Currently it's mixed
@@ -760,10 +940,6 @@ const controls = {
// Get the list to populate
const list = this.elements.settings.panes.speed.querySelector('ul');
// Show the pane and tab
utils.toggleHidden(this.elements.settings.tabs.speed, false);
utils.toggleHidden(this.elements.settings.panes.speed, false);
// Empty the menu
utils.emptyElement(list);
@@ -794,7 +970,7 @@ const controls = {
return;
}
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
if (utils.is.event(event)) {
const isMenuItem = utils.is.element(form) && form.contains(event.target);
@@ -819,7 +995,7 @@ const controls = {
}
if (utils.is.element(form)) {
form.setAttribute('aria-hidden', !show);
utils.toggleHidden(form, !show);
utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) {
@@ -835,7 +1011,7 @@ const controls = {
const clone = tab.cloneNode(true);
clone.style.position = 'absolute';
clone.style.opacity = 0;
clone.setAttribute('aria-hidden', false);
clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
@@ -860,11 +1036,9 @@ const controls = {
},
// Toggle Menu
showTab(event) {
showTab(target = '') {
const { menu } = this.elements.settings;
const tab = event.target;
const show = tab.getAttribute('aria-expanded') === 'false';
const pane = document.getElementById(tab.getAttribute('aria-controls'));
const pane = document.getElementById(target);
// Nothing to show, bail
if (!utils.is.element(pane)) {
@@ -879,7 +1053,7 @@ const controls = {
// Hide all other tabs
// Get other tabs
const current = menu.querySelector('[role="tabpanel"][aria-hidden="false"]');
const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
const container = current.parentNode;
// Set other toggles to be expanded false
@@ -923,12 +1097,16 @@ const controls = {
}
// Set attributes on current tab
current.setAttribute('aria-hidden', true);
utils.toggleHidden(current, true);
current.setAttribute('tabindex', -1);
// Set attributes on target
pane.setAttribute('aria-hidden', !show);
tab.setAttribute('aria-expanded', show);
utils.toggleHidden(pane, false);
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex');
// Focus the first item
@@ -987,7 +1165,6 @@ const controls = {
const tooltip = utils.createElement(
'span',
{
role: 'tooltip',
class: this.config.classNames.tooltip,
},
'00:00',
@@ -1069,7 +1246,7 @@ const controls = {
const form = utils.createElement('form', {
class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`,
'aria-hidden': true,
hidden: '',
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tablist',
tabindex: -1,
@@ -1079,7 +1256,6 @@ const controls = {
const home = utils.createElement('div', {
id: `plyr-settings-${data.id}-home`,
'aria-hidden': false,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel',
});
@@ -1130,11 +1306,10 @@ const controls = {
this.config.settings.forEach(type => {
const pane = utils.createElement('div', {
id: `plyr-settings-${data.id}-${type}`,
'aria-hidden': true,
hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
role: 'tabpanel',
tabindex: -1,
hidden: '',
});
const back = utils.createElement(
@@ -1205,7 +1380,7 @@ const controls = {
const icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX
if (icon.absolute) {
if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr');
}
}
@@ -1217,17 +1392,21 @@ const controls = {
let container = null;
this.elements.controls = null;
// HTML or Element passed as the option
// Set template properties
const props = {
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
};
let update = true;
if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) {
// String or HTMLElement passed as the option
container = this.config.controls;
} else if (utils.is.function(this.config.controls)) {
// A custom function to build controls
// The function can return a HTMLElement or String
container = this.config.controls({
id: this.id,
seektime: this.config.seekTime,
title: this.config.title,
});
container = this.config.controls.call(this, props);
} else {
// Create controls
container = controls.create.call(this, {
@@ -1239,6 +1418,30 @@ const controls = {
// TODO: Looping
// loop: 'None',
});
update = false;
}
// Replace props with their value
const replace = input => {
let result = input;
Object.entries(props).forEach(([
key,
value,
]) => {
result = utils.replaceAll(result, `{${key}}`, value);
});
return result;
};
// Update markup
if (update) {
if (utils.is.string(this.config.controls)) {
container = replace(container);
} else if (utils.is.element(container)) {
container.innerHTML = replace(container.innerHTML);
}
}
// Controls container
@@ -1263,7 +1466,7 @@ const controls = {
// Find the elements if need be
if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this);
controls.findElements.call(this);
}
// Edge sometimes doesn't finish the paint so force a redraw
+20 -14
View File
@@ -47,8 +47,8 @@ const defaults = {
// Auto hide the controls
hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload)
showPosterOnEnd: false,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
@@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.2.1/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.3.9/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@@ -157,10 +157,10 @@ const defaults = {
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
@@ -192,13 +192,16 @@ const defaults = {
// URLs
urls: {
vimeo: {
api: 'https://player.vimeo.com/api/player.js',
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
api: 'https://www.youtube.com/iframe_api',
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',
},
googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
@@ -322,16 +325,19 @@ const defaults = {
// Class hooks added to the player in different states
classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}',
provider: 'plyr--{0}',
stopped: 'plyr--stopped',
video: 'plyr__video-wrapper',
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',
+7 -7
View File
@@ -19,7 +19,7 @@ function onChange() {
}
// Trigger an event
utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
utils.dispatchEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
// Trap focus in container
if (!browser.isIos) {
@@ -55,7 +55,7 @@ class Fullscreen {
// Get prefix
this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name;
this.property = Fullscreen.property;
// Scroll position
this.scrollPosition = { x: 0, y: 0 };
@@ -70,7 +70,7 @@ class Fullscreen {
// Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (this.player.elements.controls.contains(event.target)) {
if (utils.is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
@@ -113,7 +113,7 @@ class Fullscreen {
return value;
}
static get name() {
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
@@ -138,7 +138,7 @@ class Fullscreen {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element === this.target;
}
@@ -176,7 +176,7 @@ class Fullscreen {
} else if (!this.prefix) {
this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.name}`]();
this.target[`${this.prefix}Request${this.property}`]();
}
}
@@ -196,7 +196,7 @@ class Fullscreen {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!utils.is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.name}`]();
document[`${this.prefix}${action}${this.property}`]();
}
}
+130 -41
View File
@@ -2,9 +2,9 @@
// Plyr Event Listeners
// ==========================================================================
import utils from './utils';
import controls from './controls';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
@@ -238,22 +238,45 @@ class Listeners {
}, 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', event => {
this.player.toggleControls(event);
});
}
// Toggle controls on mouse events and entering fullscreen
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
const { controls } = this.player.elements;
// Remove button states for fullscreen
if (event.type === 'enterfullscreen') {
controls.pressed = false;
controls.hover = false;
}
// Show, then hide after a timeout unless another control event occurs
const show = [
'touchstart',
'touchmove',
'mousemove',
].includes(event.type);
let delay = 0;
if (show) {
ui.toggleControls.call(this.player, true);
// Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000;
}
// Clear timer
clearTimeout(this.player.timers.controls);
// Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
});
}
// Listen for media events
media() {
// Time change on media
utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event));
// Display duration
utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event));
// Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
@@ -265,30 +288,24 @@ class Listeners {
// Handle the media finishing
utils.on(this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
this.player.restart();
// Re-load media
this.player.media.load();
}
});
// Check for buffer progress
utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event));
// Handle volume changes
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event));
// Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.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', () => {
@@ -492,12 +509,19 @@ class Listeners {
on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Go back to home tab on click
const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(
event,
() => {
this.player.language = event.target.value;
showHomeTab();
},
'language',
);
@@ -506,6 +530,7 @@ class Listeners {
event,
() => {
this.player.quality = event.target.value;
showHomeTab();
},
'quality',
);
@@ -514,11 +539,44 @@ class Listeners {
event,
() => {
this.player.speed = parseFloat(event.target.value);
showHomeTab();
},
'speed',
);
} else {
controls.showTab.call(this.player, event);
const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
}
});
// Set range input alternative "value", which matches the tooltip time (#954)
on(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect();
const 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', event => {
const seek = event.currentTarget;
// Was playing before?
const play = seek.hasAttribute('play-on-seeked');
// Done seeking
const 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');
this.player.play();
} else if (!done && this.player.playing) {
seek.setAttribute('play-on-seeked', '');
this.player.pause();
}
});
@@ -527,7 +585,18 @@ class Listeners {
this.player.elements.inputs.seek,
inputEvent,
event => {
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
const seek = event.currentTarget;
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
let seekTo = seek.getAttribute('seek-value');
if (utils.is.empty(seekTo)) {
seekTo = seek.value;
}
seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration;
},
'seek',
);
@@ -542,7 +611,8 @@ class Listeners {
}
this.player.config.invertTime = !this.player.config.invertTime;
ui.timeUpdate.call(this.player);
controls.timeUpdate.call(this.player);
});
}
@@ -566,26 +636,45 @@ class Listeners {
// Seek tooltip
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.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', event => {
this.player.elements.controls.hover = !this.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', event => {
this.player.elements.controls.hover = !this.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', event => {
this.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', event => {
this.player.elements.controls.pressed = [
'mousedown',
'touchstart',
].includes(event.type);
});
// Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => {
this.player.toggleControls(event);
});
}
// Focus in/out on controls
on(this.player.elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player;
// Skip transition to prevent focus from scrolling the parent element
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
// Toggle
ui.toggleControls.call(this.player, event.type === 'focusin');
// If focusin, hide again after delay
if (event.type === 'focusin') {
// Restore transition
setTimeout(() => {
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
}, 0);
// Delay a little more for keyboard users
const delay = this.touch ? 3000 : 4000;
// Clear timer
clearTimeout(timers.controls);
// Hide
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
}
});
// Mouse wheel for volume
on(
+10 -27
View File
@@ -2,15 +2,10 @@
// Plyr Media
// ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
import html5 from './html5';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import utils from './utils';
const media = {
// Setup media
@@ -33,23 +28,6 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
if (this.supported.ui) {
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// If there's no autoplay attribute, assume the video is stopped and add state class
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
@@ -59,6 +37,13 @@ const media = {
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isEmbed) {
@@ -75,8 +60,6 @@ const media = {
break;
}
} else if (this.isHTML5) {
ui.setTitle.call(this);
html5.extend.call(this);
}
},
+10 -4
View File
@@ -6,8 +6,8 @@
/* global google */
import utils from '../utils';
import i18n from '../i18n';
import utils from '../utils';
class Ads {
/**
@@ -18,7 +18,6 @@ class Ads {
constructor(player) {
this.player = player;
this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false;
this.initialized = false;
this.elements = {
@@ -44,6 +43,10 @@ class Ads {
this.load();
}
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
}
/**
* Load the IMA SDK
*/
@@ -52,7 +55,7 @@ class Ads {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.api)
.loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@@ -160,6 +163,9 @@ class Ads {
// We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false;
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.loader.requestAds(request);
} catch (e) {
this.onAdError(e);
@@ -226,7 +232,7 @@ class Ads {
// Get skippable state
// TODO: Skip button
// this.manager.getAdSkippableState();
// this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player
this.manager.setVolume(this.player.volume);
+57 -36
View File
@@ -2,10 +2,18 @@
// Vimeo plugin
// ==========================================================================
import utils from './../utils';
import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
// 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');
}
}
const vimeo = {
setup() {
@@ -18,7 +26,7 @@ const vimeo = {
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.api)
.loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@@ -53,6 +61,7 @@ const vimeo = {
const options = {
loop: player.config.loop.active,
autoplay: player.autoplay,
// muted: player.muted,
byline: false,
portrait: false,
title: false,
@@ -68,27 +77,46 @@ const vimeo = {
// Get from <div> if needed
if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`;
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Inject the package
const wrapper = utils.createElement('div');
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
ui.setPoster.call(player, url.href);
});
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe);
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true;
player.media.currentTime = 0;
@@ -100,15 +128,13 @@ const vimeo = {
// Create a faux HTML5 API using the Vimeo API
player.media.play = () => {
player.embed.play().then(() => {
player.media.paused = false;
});
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => {
player.embed.pause().then(() => {
player.media.paused = true;
});
assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
@@ -123,25 +149,26 @@ const vimeo = {
return currentTime;
},
set(time) {
// Get current paused state
// Vimeo will automatically play on seek
const { paused } = player.media;
// Vimeo will automatically play on seek if the video hasn't been played before
// Set seeking flag
player.media.seeking = true;
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
// Trigger seeking
utils.dispatchEvent.call(player, player.media, 'seeking');
// Set seeking state and trigger event
media.seeking = true;
utils.dispatchEvent.call(player, media, 'seeking');
// Seek after events
player.embed.setCurrentTime(time).catch(() => {
// Do nothing
});
// Restore pause state
if (paused) {
player.pause();
}
// If paused, mute until seek is complete
Promise.resolve(paused && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => paused && embed.pause())
// Restore volume
.then(() => paused && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
@@ -295,17 +322,12 @@ const vimeo = {
});
player.embed.on('play', () => {
// 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', () => {
player.media.paused = true;
utils.dispatchEvent.call(player, player.media, 'pause');
assurePlaybackState.call(player, false);
});
player.embed.on('timeupdate', data => {
@@ -336,7 +358,6 @@ const vimeo = {
player.embed.on('seeked', () => {
player.media.seeking = false;
utils.dispatchEvent.call(player, player.media, 'seeked');
utils.dispatchEvent.call(player, player.media, 'play');
});
player.embed.on('ended', () => {
+72 -44
View File
@@ -2,9 +2,9 @@
// YouTube plugin
// ==========================================================================
import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
@@ -64,6 +64,14 @@ function mapQualityUnits(levels) {
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
}
// 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');
}
}
const youtube = {
setup() {
// Add embed class for responsive
@@ -77,7 +85,7 @@ const youtube = {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.api).catch(error => {
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@@ -117,7 +125,7 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
const url = utils.format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
@@ -161,6 +169,21 @@ const youtube = {
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Set poster image
const posterSrc = format => `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(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
.catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
.then(image => ui.setPoster.call(player, image.src))
.then(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
player.embed = new window.YT.Player(id, {
@@ -249,10 +272,12 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
@@ -270,22 +295,17 @@ const youtube = {
return Number(instance.getCurrentTime());
},
set(time) {
// Vimeo will automatically play on seek
const { paused } = player.media;
// 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 flag
// 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();
}
},
});
@@ -404,6 +424,17 @@ const youtube = {
// Reset timer
clearInterval(player.timers.playing);
const 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
@@ -423,7 +454,7 @@ const youtube = {
break;
case 0:
player.media.paused = true;
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@@ -437,42 +468,39 @@ const 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.pause();
} else {
assurePlaybackState.call(player, true);
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
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');
}
// Get quality
controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
}
player.media.paused = false;
utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
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');
}
// 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.call(player, false);
break;
+83 -158
View File
@@ -1,26 +1,24 @@
// ==========================================================================
// Plyr
// plyr.js v3.2.1
// plyr.js v3.3.9
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import { providers, types } from './types';
import defaults from './defaults';
import support from './support';
import utils from './utils';
import captions from './captions';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import media from './media';
import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui';
import utils from './utils';
// Private properties
// TODO: Use a WeakMap for private globals
@@ -57,6 +55,7 @@ class Plyr {
this.config = utils.extend(
{},
defaults,
Plyr.defaults,
options || {},
(() => {
try {
@@ -134,17 +133,9 @@ class Plyr {
}
// Cache original element state for .destroy()
// TODO: Investigate a better solution as I suspect this causes reported double load issues?
setTimeout(() => {
const clone = this.media.cloneNode(true);
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
this.elements.original = clone;
}, 0);
const clone = this.media.cloneNode(true);
clone.autoplay = false;
this.elements.original = clone;
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
@@ -343,11 +334,6 @@ class Plyr {
return null;
}
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5)
return this.media.play();
}
@@ -363,6 +349,13 @@ class Plyr {
this.media.pause();
}
/**
* Get playing state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended);
}
/**
* Get paused state
*/
@@ -371,10 +364,10 @@ class Plyr {
}
/**
* Get playing state
* Get stopped state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
get stopped() {
return Boolean(this.paused && this.currentTime === 0);
}
/**
@@ -404,7 +397,8 @@ class Plyr {
*/
stop() {
if (this.isHTML5) {
this.media.load();
this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) {
this.media.stop();
}
@@ -438,21 +432,16 @@ class Plyr {
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
*/
set currentTime(input) {
let targetTime = 0;
if (utils.is.number(input)) {
targetTime = input;
// Bail if media duration isn't available yet
if (!this.duration) {
return;
}
// Normalise targetTime
if (targetTime < 0) {
targetTime = 0;
} else if (targetTime > this.duration) {
targetTime = this.duration;
}
// Validate input
const inputIsValid = utils.is.number(input) && input > 0;
// Set
this.media.currentTime = targetTime;
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
// Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`);
@@ -500,11 +489,11 @@ class Plyr {
// Faux duration set via config
const fauxDuration = parseFloat(this.config.duration);
// True duration
const realDuration = this.media ? Number(this.media.duration) : 0;
// Media duration can be NaN before the media has loaded
const duration = (this.media || {}).duration || 0;
// If custom duration is funky, use regular duration
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
// If config duration is funky, use regular duration
return fauxDuration || duration;
}
/**
@@ -799,25 +788,23 @@ class Plyr {
}
/**
* Set the poster image for a HTML5 video
* Set the poster image for a video
* @param {input} - the URL for the new poster image
*/
set poster(input) {
if (!this.isHTML5 || !this.isVideo) {
this.debug.warn('Poster can only be set on HTML5 video');
if (!this.isVideo) {
this.debug.warn('Poster can only be set for video');
return;
}
if (utils.is.string(input)) {
this.media.setAttribute('poster', input);
}
ui.setPoster.call(this, input);
}
/**
* Get the current poster image
*/
get poster() {
if (!this.isHTML5 || !this.isVideo) {
if (!this.isVideo) {
return null;
}
@@ -976,119 +963,32 @@ class Plyr {
/**
* Toggle the player controls
* @param {boolean} toggle - Whether to show the controls
* @param {boolean} [toggle] - Whether to show the controls
*/
toggleControls(toggle) {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// Don't toggle if missing UI support or if it's audio
if (this.supported.ui && !this.isAudio) {
// Get state before change
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
// Don't hide if no UI support or it's audio
if (!this.supported.ui || this.isAudio) {
return;
}
// Negate the argument if not undefined since adding the class to hides the controls
const force = typeof toggle === 'undefined' ? undefined : !toggle;
let delay = 0;
let show = toggle;
let isEnterFullscreen = false;
// Apply and get updated state
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
// 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
const showEvents = [
'touchstart',
'touchmove',
'mouseenter',
'mousemove',
'focusin',
];
// Events that delay hiding
const delayEvents = [
'touchmove',
'touchend',
'mousemove',
];
// Whether to show controls
show = showEvents.includes(toggle.type);
// 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);
// Close menu
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
}
// 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
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
// Trigger event
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlsshown');
}
// Always show controls when paused or if touch
if (this.paused || this.loading) {
return;
}
// Delay for hiding on touch
if (this.touch) {
delay = 3000;
// Trigger event on change
if (hiding !== isHidden) {
const 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(() => {
// We need controls of course...
if (!utils.is.element(this.elements.controls)) {
return;
}
// If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
return;
}
// Restore transition behaviour
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
// Trigger event and close menu
if (toggled) {
utils.dispatchEvent.call(this, this.media, 'controlshidden');
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false);
}
}
}, delay);
}
return false;
}
/**
@@ -1250,6 +1150,31 @@ class Plyr {
static loadSprite(url, id) {
return utils.loadSprite(url, id);
}
/**
* Setup multiple instances
* @param {*} selector
* @param {object} options
*/
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(i => utils.is.element(i));
}
if (utils.is.empty(targets)) {
return null;
}
return targets.map(t => new Plyr(t, options));
}
}
Plyr.defaults = utils.cloneDeep(defaults);
export default Plyr;
+2 -3
View File
@@ -1,14 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.2.1
// plyr.js v3.3.9
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;
+5 -5
View File
@@ -2,12 +2,12 @@
// Plyr source update
// ==========================================================================
import { providers } from './types';
import utils from './utils';
import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
@@ -94,8 +94,8 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if ('poster' in input) {
this.media.setAttribute('poster', input.poster);
if (!utils.is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
+1 -1
View File
@@ -31,7 +31,7 @@ class Storage {
}
get(key) {
if (!Storage.supported) {
if (!Storage.supported || !this.enabled) {
return null;
}
+1
View File
@@ -133,6 +133,7 @@ const support = {
},
});
window.addEventListener('test', null, options);
window.removeEventListener('test', null, options);
} catch (e) {
// Do nothing
}
+79 -170
View File
@@ -2,10 +2,14 @@
// Plyr UI
// ==========================================================================
import utils from './utils';
import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const ui = {
addStyleHook() {
@@ -48,11 +52,6 @@ const ui = {
this.listeners.controls();
}
// If there's no controls, bail
if (!utils.is.element(this.elements.controls)) {
return;
}
// Remove native controls
ui.toggleNativeControls.call(this);
@@ -75,14 +74,26 @@ const ui = {
this.quality = null;
// Reset volume display
ui.updateVolume.call(this);
controls.updateVolume.call(this);
// Reset time display
ui.timeUpdate.call(this);
controls.timeUpdate.call(this);
// Update the UI
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
@@ -93,6 +104,11 @@ const ui = {
// Set the title
ui.setTitle.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);
}
},
// Setup aria attribute for play and iframe title
@@ -126,22 +142,64 @@ const ui = {
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Toggle poster
togglePoster(enable) {
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
},
// Set the poster image (async)
setPoster(poster) {
// 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();
}
// Load the image, and set poster if successful
const loadPromise = utils.loadImage(poster)
.then(() => {
this.elements.poster.style.backgroundImage = `url('${poster}')`;
Object.assign(this.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(this, 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(() => ui.togglePoster.call(this, false));
// Return the promise so the caller can use it as well
return loadPromise;
},
// Check playing state
checkPlaying() {
checkPlaying(event) {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
// Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls
this.toggleControls(!this.playing);
ui.toggleControls.call(this);
},
// Check if media is loading
@@ -156,171 +214,22 @@ const ui = {
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
// Update progress bar loading class state
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
// Update controls visibility
ui.toggleControls.call(this);
}, this.loading ? 250 : 0);
},
// Check if media failed to load
checkFailed() {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
this.failed = this.media.networkState === 3;
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls } = this.elements;
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 && 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));
}
// Clear timer
clearTimeout(this.timers.failed);
// Timer to prevent flicker when seeking
this.timers.loading = setTimeout(() => {
// Toggle container class hook
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
// Show controls if loading, hide if done
this.toggleControls(this.loading);
}, this.loading ? 250 : 0);
},
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.element(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update mute state
if (utils.is.element(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill
setRange(target, value = 0) {
if (!utils.is.element(target)) {
return;
}
// eslint-disable-next-line
target.value = value;
// Webkit range fill
controls.updateRangeFill.call(this, target);
},
// Set <progress> value
setProgress(target, input) {
const value = utils.is.number(input) ? input : 0;
const progress = utils.is.element(target) ? target : this.elements.display.buffer;
// Update value and label
if (utils.is.element(progress)) {
progress.value = value;
// Update text label inside
const label = progress.getElementsByTagName('span')[0];
if (utils.is.element(label)) {
label.childNodes[0].nodeValue = value;
}
}
},
// Update <progress> elements
updateProgress(event) {
if (!this.supported.ui || !utils.is.event(event)) {
return;
}
let value = 0;
if (event) {
switch (event.type) {
// Video playing
case 'timeupdate':
case 'seeking':
value = utils.getPercentage(this.currentTime, this.duration);
// Set seek range value only if it's a 'natural' time event
if (event.type === 'timeupdate') {
ui.setRange.call(this, this.elements.inputs.seek, value);
}
break;
// Check buffer status
case 'playing':
case 'progress':
ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
break;
default:
break;
}
}
},
// Update the displayed time
updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no element to display or the value isn't a number
if (!utils.is.element(target) || !utils.is.number(time)) {
return;
}
// Always display hours if duration is over an hour
const displayHours = utils.getHours(this.duration) > 0;
// eslint-disable-next-line no-param-reassign
target.textContent = utils.formatTime(time, displayHours, inverted);
},
// Handle time change event
timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
// Duration
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
// Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// Playing progress
ui.updateProgress.call(this, event);
},
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's a spot to display duration
const hasDuration = utils.is.element(this.elements.display.duration);
// If there's only one time display, display duration there
if (!hasDuration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (hasDuration) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
};
+73 -91
View File
@@ -3,16 +3,13 @@
// ==========================================================================
import loadjs from 'loadjs';
import Storage from './storage';
import support from './support';
import { providers } from './types';
const utils = {
// Check variable types
is: {
plyr(input) {
return this.instanceof(input, window.Plyr);
},
object(input) {
return this.getConstructor(input) === Object;
},
@@ -32,19 +29,19 @@ const utils = {
return !this.nullOrUndefined(input) && Array.isArray(input);
},
weakMap(input) {
return this.instanceof(input, window.WeakMap);
return this.instanceof(input, WeakMap);
},
nodeList(input) {
return this.instanceof(input, window.NodeList);
return this.instanceof(input, NodeList);
},
element(input) {
return this.instanceof(input, window.Element);
return this.instanceof(input, Element);
},
textNode(input) {
return this.getConstructor(input) === Text;
},
event(input) {
return this.instanceof(input, window.Event);
return this.instanceof(input, Event);
},
cue(input) {
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
@@ -123,6 +120,21 @@ const 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(src, minWidth = 1) {
return new Promise((resolve, reject) => {
const image = new Image();
const handler = () => {
delete image.onload;
delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image);
};
Object.assign(image, {onload: handler, onerror: handler, src});
});
},
// Load an external script
loadScript(url) {
return new Promise((resolve, reject) => {
@@ -160,6 +172,8 @@ const utils = {
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
utils.toggleHidden(container, true);
@@ -169,7 +183,7 @@ const utils = {
}
// Check in cache
if (support.storage) {
if (useStorage) {
const cached = window.localStorage.getItem(prefix + id);
isCached = cached !== null;
@@ -188,7 +202,7 @@ const utils = {
return;
}
if (support.storage) {
if (useStorage) {
window.localStorage.setItem(
prefix + id,
JSON.stringify({
@@ -251,7 +265,7 @@ const utils = {
// Add text node
if (utils.is.string(text)) {
element.textContent = text;
element.innerText = text;
}
// Return built element
@@ -269,14 +283,14 @@ const utils = {
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove an element
// Remove element(s)
removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
@@ -375,14 +389,35 @@ const utils = {
return attributes;
},
// Toggle class on an element
toggleClass(element, className, toggle) {
// Toggle hidden
toggleHidden(element, hidden) {
if (!utils.is.element(element)) {
return;
}
let hide = hidden;
if (!utils.is.boolean(hide)) {
hide = !element.hasAttribute('hidden');
}
if (hide) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
toggleClass(element, className, force) {
if (utils.is.element(element)) {
const contains = element.classList.contains(className);
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
}
element.classList[toggle ? 'add' : 'remove'](className);
return (toggle && !contains) || (!toggle && contains);
element.classList[method](className);
return element.classList.contains(className);
}
return null;
@@ -393,19 +428,6 @@ const utils = {
return utils.is.element(element) && element.classList.contains(className);
},
// Toggle hidden attribute on an element
toggleHidden(element, toggle) {
if (!utils.is.element(element)) {
return;
}
if (toggle) {
element.setAttribute('hidden', '');
} else {
element.removeAttribute('hidden');
}
},
// Element matches selector
matches(element, selector) {
const prototype = { Element };
@@ -429,60 +451,6 @@ const utils = {
return this.elements.container.querySelector(selector);
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
@@ -596,7 +564,7 @@ const utils = {
const event = new CustomEvent(type, {
bubbles,
detail: Object.assign({}, detail, {
plyr: utils.is.plyr(this) ? this : null,
plyr: this,
}),
});
@@ -626,6 +594,15 @@ const utils = {
element.setAttribute('aria-pressed', state);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
@@ -746,13 +723,18 @@ const utils = {
return array.filter((item, index) => array.indexOf(item) === index);
},
// Clone nested objects
cloneDeep(object) {
return JSON.parse(JSON.stringify(object));
},
// Get the closest value in an array
closest(array, value) {
if (!utils.is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
},
// Get the provider for a given URL
@@ -763,7 +745,7 @@ const utils = {
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}
+1
View File
@@ -29,6 +29,7 @@
button {
font: inherit;
line-height: inherit;
width: auto;
}
// Ignore focus
+2 -2
View File
@@ -32,8 +32,8 @@ $embed-padding: ((100 / 16) * 9);
pointer-events: none;
}
// Vimeo hack
> div {
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);
+23
View File
@@ -0,0 +1,23 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
pointer-events: none;
}
.plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1;
}
+5
View File
@@ -6,10 +6,15 @@
display: flex;
flex: 1;
position: relative;
margin-right: $plyr-range-thumb-height;
left: $plyr-range-thumb-height / 2;
input[type='range'] {
position: relative;
z-index: 2;
// Offset the range thumb in order to be able to calculate the relative progress (#954)
width: calc(100% + #{$plyr-range-thumb-height}) !important;
margin: 0 -#{$plyr-range-thumb-height / 2} !important;
}
// Seek tooltip to show time
+1
View File
@@ -19,6 +19,7 @@
transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap;
z-index: 2;
// The background triangle
+7 -2
View File
@@ -23,7 +23,12 @@
// Hide sound controls on iOS
// It's not supported to change volume using JavaScript:
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
.plyr--is-ios .plyr__volume,
.plyr--is-ios [data-plyr='mute'] {
.plyr--is-ios .plyr__volume {
display: none !important;
}
// Vimeo has no toggle mute method so hide mute button
// https://github.com/vimeo/player.js/issues/236#issuecomment-384663183
.plyr--is-ios.plyr--vimeo [data-plyr='mute'] {
display: none !important;
}
+1 -1
View File
@@ -32,13 +32,13 @@
@import 'components/embed';
@import 'components/menus';
@import 'components/progress';
@import 'components/poster';
@import 'components/sliders';
@import 'components/times';
@import 'components/tooltips';
@import 'components/video';
@import 'components/volume';
@import 'states/error';
@import 'states/fullscreen';
@import 'plugins/ads';
-25
View File
@@ -1,25 +0,0 @@
// --------------------------------------------------------------
// Error state
// --------------------------------------------------------------
.plyr--has-error {
pointer-events: none;
&::after {
align-items: center;
background: rgba(#000, 90%);
color: #fff;
content: attr(data-plyr-error);
display: flex;
font-size: $plyr-font-size-base;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
text-align: center;
text-shadow: 0 1px 1px rgba(#000, 10%);
top: 0;
width: 100%;
z-index: 10;
}
}
-9
View File
@@ -2,15 +2,6 @@
// Hiding content nicely
// --------------------------------------------------------------
// Attributes
.plyr--full-ui [hidden] {
display: none;
}
.plyr--full-ui [aria-hidden='true'] {
display: none;
}
// Screen reader only elements
.plyr__sr-only {
clip: rect(1px, 1px, 1px, 1px);
+1548 -80
View File
File diff suppressed because it is too large Load Diff