Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 840e31a693 | |||
| 38f954ef17 | |||
| abd1182303 | |||
| efe70ab48e | |||
| 1ad76800b0 | |||
| cc97d7be6a | |||
| f951cb372c | |||
| 37a3ab202a | |||
| b148adc0af | |||
| 16828e975a | |||
| 7d26f41d64 | |||
| f37f465ce4 | |||
| b199215525 | |||
| 94699f3255 | |||
| d3e98eb27e | |||
| 41012a9843 | |||
| c83487a293 | |||
| 1fab4919c0 | |||
| 9dc0f28800 | |||
| b57784d1a5 | |||
| a80b31bf98 | |||
| 76bb299c68 | |||
| 0c03accd41 | |||
| b12eeb0eb7 | |||
| 8e634862ff | |||
| 1e1874d86b | |||
| 16624b90d3 | |||
| ed14b656a8 | |||
| 1d0cf16254 | |||
| 84424f7f67 | |||
| c95d9923f7 | |||
| 05b85da3f4 | |||
| a4caba120c | |||
| 969a877a34 | |||
| fb22a90d33 | |||
| 108bd3dfa0 | |||
| 5a445ae647 | |||
| 56668f58b6 | |||
| eec96e5879 | |||
| c6c9d877e4 | |||
| 61f4b998e1 | |||
| 25a319d884 | |||
| 4bf678fe6c | |||
| 359acd6bb9 | |||
| 2fce385691 | |||
| a82c61c539 | |||
| a8fa125a96 | |||
| 6435ced707 | |||
| 94dc0d176c | |||
| 23c21252e8 | |||
| 41c7dff0e8 | |||
| e3bae562fc | |||
| 1c1668bfc3 | |||
| e0c09c51f2 | |||
| 8de06fb862 | |||
| 64412868d8 | |||
| 450958c290 | |||
| 963fe11ad6 | |||
| ce199e4b6b | |||
| 9d798893b5 | |||
| 64399e0717 | |||
| f58e23b325 | |||
| 812e07b734 | |||
| c9298fde76 | |||
| 0109454a34 | |||
| 813f703211 | |||
| 7aad747c25 | |||
| d70a787af1 | |||
| 69bb0917ad | |||
| 6f256d09b2 | |||
| e9684c2021 | |||
| bf91a0e73f | |||
| 14b6309aef | |||
| 6391ced99f | |||
| fac8a185ba | |||
| c69aa8a42b | |||
| f34bf22125 | |||
| f0be913dc3 | |||
| cd51788b98 | |||
| edd67b0da3 | |||
| d733454d7f | |||
| 41f9a87e0e | |||
| f4858f0c62 | |||
| 121093ae71 | |||
| aa8fc313a9 | |||
| 723298a07b | |||
| f8c89e3e95 | |||
| 333435a9c2 | |||
| 3ab2295fe7 | |||
| c41bb657ac | |||
| 55bbf64f2b | |||
| 3bba65f2c2 | |||
| 1bab0d07b5 | |||
| 602353f4d9 | |||
| 51814249af | |||
| 37c5fbfe16 | |||
| d7356726a1 | |||
| 4db6bf7a2e | |||
| 28826f6402 | |||
| c845558d96 | |||
| 16c3a7d9e5 | |||
| 90d5b48845 | |||
| d1acc4abb3 | |||
| 797b70998f | |||
| 4a01027da0 | |||
| 7ca2169790 | |||
| 054f522aa9 | |||
| f2fc3f5ea5 | |||
| 765c01e83d | |||
| 34401de3d0 | |||
| 403df36af6 | |||
| 24d833a5d1 | |||
| 44b30380f7 | |||
| f13260c10a | |||
| e1183d6049 |
@@ -32,6 +32,7 @@
|
|||||||
"message": "Use local parameter instead."
|
"message": "Use local parameter instead."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"no-param-reassign": [2, { "props": false }],
|
||||||
"array-bracket-newline": [2, { "minItems": 2 }],
|
"array-bracket-newline": [2, { "minItems": 2 }],
|
||||||
"array-element-newline": [2, { "minItems": 2 }]
|
"array-element-newline": [2, { "minItems": 2 }]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
### Link to related issue (if applicable)
|
### Link to related issue (if applicable)
|
||||||
|
|
||||||
### Sumary of proposed changes
|
### Summary of proposed changes
|
||||||
|
|
||||||
### Task list
|
### Checklist
|
||||||
|
- [ ] Use `develop` as the base branch
|
||||||
- [ ] Tested on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
- [ ] Exclude the gulp build from the PR
|
||||||
- [ ] Gulp build completed
|
- [ ] Test on [supported browsers](https://github.com/sampotts/plyr#browser-support)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 'lts/*'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- npm run lint
|
||||||
|
- npm run build
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
We welcome bug reports, feature requests and pull requests. If you want to help us out, please follow these guidelines, in order to avoid redundant work.
|
||||||
|
|
||||||
|
## Reporting issues
|
||||||
|
|
||||||
|
Our GitHub issue tracker is for bug reports and feature requests. Don't create support issues here. Use [Stack Overflow](https://stackoverflow.com/) or [our Slack](https://bit.ly/plyr-chat) for that.
|
||||||
|
|
||||||
|
Please verify that your issue hasn't already been answered by our FAQ (https://github.com/sampotts/plyr/wiki/FAQ), or that there isn't already an open issue for it.
|
||||||
|
|
||||||
|
When applicable, check that your problem doesn't happen without Plyr (see [FAQ#1](https://github.com/sampotts/plyr/wiki/FAQ#1-does-plyr-work-with--)).
|
||||||
|
|
||||||
|
Verify that you are following the documentation, are using the latest version of Plyr, and aren't getting any errors in your own code, causing the issues.
|
||||||
|
|
||||||
|
Describe the issue as detailed as possible, answering these questions:
|
||||||
|
|
||||||
|
* Does it happen only with specific options and/or specific browsers?
|
||||||
|
* Does is happen only with HTML5 video, audio, youtube, vimeo or a specific library?
|
||||||
|
* Does the issue happen on [our demo](https://plyr.io/)? If not, please recreate it with a **minimal** example online. You can use our Codepen templates to get started:
|
||||||
|
* [HTML5 video](https://codepen.io/pen?template=bKeqpr)
|
||||||
|
* [HTML5 audio](https://codepen.io/pen?template=rKLywR)
|
||||||
|
* [YouTube](https://codepen.io/pen?template=GGqbbJ)
|
||||||
|
* [Vimeo](https://codepen.io/pen?template=bKeXNq)
|
||||||
|
* [Dash.js integration](https://codepen.io/pen?template=zaBgBy)
|
||||||
|
* [Hls.js integration](https://codepen.io/pen?template=oyLKQb)
|
||||||
|
* [Shaka Player integration](https://codepen.io/pen?template=ZRpzZO)
|
||||||
|
|
||||||
|
It's important that you keep the issue description and replication demo **minimal**. If your implementation is using a framework, library or custom methods, which aren't needed to reproduce the issue, this makes it harder to debug and understand the issue. While it may be relevant to bring this up (ex: "I need Plyr to trigger the event sooner or it breaks Framework X") it also means that the person who is trying to fix the issue either has to know or learn your frameworks, libraries and custom methods, or that no one will try to fix your issue because it's too much work.
|
||||||
|
|
||||||
|
In order to keep things on topic and to avoid bothering people with github notifications, please don't combine multiple problems or bugs into one issue, don't comment on issues unless your comment is related to that issue, and don't post "+1" or "I agree" type of comments. Use the emojis instead.
|
||||||
|
|
||||||
|
Last but not least: Keep a civil tone in issues and comments. Non-constructive comments may be removed.
|
||||||
|
|
||||||
|
## Requesting features and improvements
|
||||||
|
|
||||||
|
If you are missing something in Plyr, you can create a GitHub issue for this as well. Since we prioritize fixing bugs first, and may have a lot of other suggestions and architectural changes to work on as well, these may not be at the top of our list. If it's important or urgent to you, you may want to first ensure it's something we want to have in Plyr, and then contribute it as a pull request.
|
||||||
|
|
||||||
|
## Contributing features and documentation
|
||||||
|
|
||||||
|
* Fork Plyr, and create a new branch in your fork, based on the **develop** branch
|
||||||
|
|
||||||
|
* To test locally, you can use the demo. First make sure you have installed the dependencies with `npm install` or `yarn`. Run `gulp` to build while you are working, and run a local server from the repository root directory. If you have Python installed, this command should work: `python -m SimpleHTTPServer 8080`. Then go to `http://localhost:8080/demo/`
|
||||||
|
|
||||||
|
* Develop and test your modifications.
|
||||||
|
|
||||||
|
* Preferably commit your changes as independent logical chunks, with meaningful messages. Make sure you do not commit unnecessary files or changes, such as logging or breakpoints you added for testing, and the build output.
|
||||||
|
|
||||||
|
* If your modifications changes the documented behavior or add new features, document these changes in readme.md.
|
||||||
|
|
||||||
|
* When finished, push the changes to your GitHub repository and send a pull request to **develop**. Describe what your PR does.
|
||||||
|
|
||||||
|
* If the Travis build fails, or if you get a code review with change requests, you can fix these by pushing new or rebased commits to the branch.
|
||||||
+426
-385
File diff suppressed because it is too large
Load Diff
+3
-5
@@ -116,9 +116,8 @@ const controls = `
|
|||||||
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
|
<span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="plyr__progress">
|
<div class="plyr__progress">
|
||||||
<label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label>
|
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
|
||||||
<input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}">
|
<progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
|
||||||
<progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
|
|
||||||
<span role="tooltip" class="plyr__tooltip">00:00</span>
|
<span role="tooltip" class="plyr__tooltip">00:00</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
<div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
|
||||||
@@ -130,8 +129,7 @@ const controls = `
|
|||||||
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
|
<span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="plyr__volume">
|
<div class="plyr__volume">
|
||||||
<label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
|
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" aria-label="Volume">
|
||||||
<input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
|
<button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
|
||||||
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
<svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+207
-25
@@ -1,4 +1,4 @@
|
|||||||
(function () {
|
typeof navigator === "object" && (function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
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
|
// Yanked from https://git.io/vS8DV re-used under CC0
|
||||||
// with some tiny modifications
|
// with some tiny modifications
|
||||||
function isError(value) {
|
function isError(value) {
|
||||||
switch ({}.toString.call(value)) {
|
switch (Object.prototype.toString.call(value)) {
|
||||||
case '[object Error]':
|
case '[object Error]':
|
||||||
return true;
|
return true;
|
||||||
case '[object Exception]':
|
case '[object Exception]':
|
||||||
@@ -110,7 +110,15 @@ function isError(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isErrorEvent(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) {
|
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() {
|
function supportsFetch() {
|
||||||
if (!('fetch' in _window)) return false;
|
if (!('fetch' in _window)) return false;
|
||||||
|
|
||||||
@@ -668,6 +694,8 @@ var utils = {
|
|||||||
isObject: isObject,
|
isObject: isObject,
|
||||||
isError: isError,
|
isError: isError,
|
||||||
isErrorEvent: isErrorEvent,
|
isErrorEvent: isErrorEvent,
|
||||||
|
isDOMError: isDOMError,
|
||||||
|
isDOMException: isDOMException,
|
||||||
isUndefined: isUndefined,
|
isUndefined: isUndefined,
|
||||||
isFunction: isFunction,
|
isFunction: isFunction,
|
||||||
isPlainObject: isPlainObject,
|
isPlainObject: isPlainObject,
|
||||||
@@ -675,6 +703,8 @@ var utils = {
|
|||||||
isArray: isArray,
|
isArray: isArray,
|
||||||
isEmptyObject: isEmptyObject,
|
isEmptyObject: isEmptyObject,
|
||||||
supportsErrorEvent: supportsErrorEvent,
|
supportsErrorEvent: supportsErrorEvent,
|
||||||
|
supportsDOMError: supportsDOMError,
|
||||||
|
supportsDOMException: supportsDOMException,
|
||||||
supportsFetch: supportsFetch,
|
supportsFetch: supportsFetch,
|
||||||
supportsReferrerPolicy: supportsReferrerPolicy,
|
supportsReferrerPolicy: supportsReferrerPolicy,
|
||||||
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
|
supportsPromiseRejectionEvent: supportsPromiseRejectionEvent,
|
||||||
@@ -729,10 +759,25 @@ var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Ran
|
|||||||
|
|
||||||
function getLocationHref() {
|
function getLocationHref() {
|
||||||
if (typeof document === 'undefined' || document.location == null) return '';
|
if (typeof document === 'undefined' || document.location == null) return '';
|
||||||
|
|
||||||
return document.location.href;
|
return document.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLocationOrigin() {
|
||||||
|
if (typeof document === 'undefined' || document.location == null) return '';
|
||||||
|
|
||||||
|
// Oh dear IE10...
|
||||||
|
if (!document.location.origin) {
|
||||||
|
return (
|
||||||
|
document.location.protocol +
|
||||||
|
'//' +
|
||||||
|
document.location.hostname +
|
||||||
|
(document.location.port ? ':' + document.location.port : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TraceKit.report: cross-browser processing of unhandled exceptions
|
* TraceKit.report: cross-browser processing of unhandled exceptions
|
||||||
*
|
*
|
||||||
@@ -1140,6 +1185,44 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
|
|||||||
element.func = UNKNOWN_FUNCTION;
|
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);
|
stack.push(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,10 +1734,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 isError$1 = utils.isError;
|
||||||
var isObject$1 = utils.isObject;
|
var isObject$1 = utils.isObject;
|
||||||
var isPlainObject$1 = utils.isPlainObject;
|
var isPlainObject$1 = utils.isPlainObject;
|
||||||
var isErrorEvent$1 = utils.isErrorEvent;
|
|
||||||
var isUndefined$1 = utils.isUndefined;
|
var isUndefined$1 = utils.isUndefined;
|
||||||
var isFunction$1 = utils.isFunction;
|
var isFunction$1 = utils.isFunction;
|
||||||
var isString$1 = utils.isString;
|
var isString$1 = utils.isString;
|
||||||
@@ -1782,7 +1867,7 @@ Raven.prototype = {
|
|||||||
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
||||||
// this value matches package.json during build.
|
// this value matches package.json during build.
|
||||||
// See: https://github.com/getsentry/raven-js/issues/465
|
// See: https://github.com/getsentry/raven-js/issues/465
|
||||||
VERSION: '3.24.2',
|
VERSION: '3.26.1',
|
||||||
|
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
@@ -1948,7 +2033,7 @@ Raven.prototype = {
|
|||||||
if (isFunction$1(options)) {
|
if (isFunction$1(options)) {
|
||||||
args = func || [];
|
args = func || [];
|
||||||
func = options;
|
func = options;
|
||||||
options = undefined;
|
options = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.wrap(options, func).apply(this, args);
|
return this.wrap(options, func).apply(this, args);
|
||||||
@@ -1959,7 +2044,7 @@ Raven.prototype = {
|
|||||||
*
|
*
|
||||||
* @param {object} options A specific set of options for this context [optional]
|
* @param {object} options A specific set of options for this context [optional]
|
||||||
* @param {function} func The function to be wrapped in a new context
|
* @param {function} func The function to be wrapped in a new context
|
||||||
* @param {function} func A function to call before the try/catch wrapper [optional, private]
|
* @param {function} _before A function to call before the try/catch wrapper [optional, private]
|
||||||
* @return {function} The newly wrapped functions with a context
|
* @return {function} The newly wrapped functions with a context
|
||||||
*/
|
*/
|
||||||
wrap: function(options, func, _before) {
|
wrap: function(options, func, _before) {
|
||||||
@@ -2072,8 +2157,9 @@ Raven.prototype = {
|
|||||||
_promiseRejectionHandler: function(event) {
|
_promiseRejectionHandler: function(event) {
|
||||||
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
|
this._logDebug('debug', 'Raven caught unhandled promise rejection:', event);
|
||||||
this.captureException(event.reason, {
|
this.captureException(event.reason, {
|
||||||
extra: {
|
mechanism: {
|
||||||
unhandledPromiseRejection: true
|
type: 'onunhandledrejection',
|
||||||
|
handled: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -2114,6 +2200,23 @@ Raven.prototype = {
|
|||||||
if (isErrorEvent$1(ex) && ex.error) {
|
if (isErrorEvent$1(ex) && ex.error) {
|
||||||
// If it is an ErrorEvent with `error` property, extract it to get actual Error
|
// If it is an ErrorEvent with `error` property, extract it to get actual Error
|
||||||
ex = ex.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)) {
|
} else if (isError$1(ex)) {
|
||||||
// we have a real Error object
|
// we have a real Error object
|
||||||
ex = ex;
|
ex = ex;
|
||||||
@@ -2125,6 +2228,7 @@ Raven.prototype = {
|
|||||||
ex = new Error(options.message);
|
ex = new Error(options.message);
|
||||||
} else {
|
} else {
|
||||||
// If none of previous checks were valid, then it means that
|
// 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 plain Object
|
||||||
// it's not a valid ErrorEvent (one with an error property)
|
// it's not a valid ErrorEvent (one with an error property)
|
||||||
// it's not an Error
|
// it's not an Error
|
||||||
@@ -2732,7 +2836,15 @@ Raven.prototype = {
|
|||||||
}
|
}
|
||||||
var originalCallback = args[0];
|
var originalCallback = args[0];
|
||||||
if (isFunction$1(originalCallback)) {
|
if (isFunction$1(originalCallback)) {
|
||||||
args[0] = self.wrap(originalCallback);
|
args[0] = self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {function: orig.name}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
originalCallback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
|
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
|
||||||
@@ -2759,7 +2871,15 @@ Raven.prototype = {
|
|||||||
// preserve arity
|
// preserve arity
|
||||||
try {
|
try {
|
||||||
if (fn && fn.handleEvent) {
|
if (fn && fn.handleEvent) {
|
||||||
fn.handleEvent = self.wrap(fn.handleEvent);
|
fn.handleEvent = self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {target: global, function: 'handleEvent', handler: fn.name}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fn.handleEvent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// can sometimes get 'Permission denied to access property "handle Event'
|
// can sometimes get 'Permission denied to access property "handle Event'
|
||||||
@@ -2799,7 +2919,20 @@ Raven.prototype = {
|
|||||||
return orig.call(
|
return orig.call(
|
||||||
this,
|
this,
|
||||||
evtName,
|
evtName,
|
||||||
self.wrap(fn, undefined, before),
|
self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {
|
||||||
|
target: global,
|
||||||
|
function: 'addEventListener',
|
||||||
|
handler: fn.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fn,
|
||||||
|
before
|
||||||
|
),
|
||||||
capture,
|
capture,
|
||||||
secure
|
secure
|
||||||
);
|
);
|
||||||
@@ -2833,7 +2966,17 @@ Raven.prototype = {
|
|||||||
'requestAnimationFrame',
|
'requestAnimationFrame',
|
||||||
function(orig) {
|
function(orig) {
|
||||||
return function(cb) {
|
return function(cb) {
|
||||||
return orig(self.wrap(cb));
|
return orig(
|
||||||
|
self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {function: 'requestAnimationFrame', handler: orig.name}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
wrappedBuiltIns
|
wrappedBuiltIns
|
||||||
@@ -2896,7 +3039,15 @@ Raven.prototype = {
|
|||||||
function wrapProp(prop, xhr) {
|
function wrapProp(prop, xhr) {
|
||||||
if (prop in xhr && isFunction$1(xhr[prop])) {
|
if (prop in xhr && isFunction$1(xhr[prop])) {
|
||||||
fill$1(xhr, prop, function(orig) {
|
fill$1(xhr, prop, function(orig) {
|
||||||
return self.wrap(orig);
|
return self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {function: prop, handler: orig.name}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orig
|
||||||
|
);
|
||||||
}); // intentionally don't track filled methods on XHR instances
|
}); // intentionally don't track filled methods on XHR instances
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2961,7 +3112,19 @@ Raven.prototype = {
|
|||||||
xhr,
|
xhr,
|
||||||
'onreadystatechange',
|
'onreadystatechange',
|
||||||
function(orig) {
|
function(orig) {
|
||||||
return self.wrap(orig, undefined, onreadystatechangeHandler);
|
return self.wrap(
|
||||||
|
{
|
||||||
|
mechanism: {
|
||||||
|
type: 'instrument',
|
||||||
|
data: {
|
||||||
|
function: 'onreadystatechange',
|
||||||
|
handler: orig.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orig,
|
||||||
|
onreadystatechangeHandler
|
||||||
|
);
|
||||||
} /* intentionally don't track this instrumentation */
|
} /* intentionally don't track this instrumentation */
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -3073,8 +3236,8 @@ Raven.prototype = {
|
|||||||
var hasPushAndReplaceState =
|
var hasPushAndReplaceState =
|
||||||
!isChromePackagedApp &&
|
!isChromePackagedApp &&
|
||||||
_window$2.history &&
|
_window$2.history &&
|
||||||
history.pushState &&
|
_window$2.history.pushState &&
|
||||||
history.replaceState;
|
_window$2.history.replaceState;
|
||||||
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
|
if (autoBreadcrumbs.location && hasPushAndReplaceState) {
|
||||||
// TODO: remove onpopstate handler on uninstall()
|
// TODO: remove onpopstate handler on uninstall()
|
||||||
var oldOnPopState = _window$2.onpopstate;
|
var oldOnPopState = _window$2.onpopstate;
|
||||||
@@ -3103,8 +3266,8 @@ Raven.prototype = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fill$1(history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
fill$1(_window$2.history, 'pushState', historyReplacementFunction, wrappedBuiltIns);
|
||||||
fill$1(history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
fill$1(_window$2.history, 'replaceState', historyReplacementFunction, wrappedBuiltIns);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
|
if (autoBreadcrumbs.console && 'console' in _window$2 && console.log) {
|
||||||
@@ -3185,10 +3348,16 @@ Raven.prototype = {
|
|||||||
return globalServer;
|
return globalServer;
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleOnErrorStackInfo: function() {
|
_handleOnErrorStackInfo: function(stackInfo, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.mechanism = options.mechanism || {
|
||||||
|
type: 'onerror',
|
||||||
|
handled: false
|
||||||
|
};
|
||||||
|
|
||||||
// if we are intentionally ignoring errors via onerror, bail out
|
// if we are intentionally ignoring errors via onerror, bail out
|
||||||
if (!this._ignoreOnError) {
|
if (!this._ignoreOnError) {
|
||||||
this._handleStackInfo.apply(this, arguments);
|
this._handleStackInfo(stackInfo, options);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -3320,11 +3489,24 @@ Raven.prototype = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
culprit: fileurl
|
transaction: fileurl
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Move mechanism from options to exception interface
|
||||||
|
// We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be
|
||||||
|
// too much
|
||||||
|
if (!data.exception.mechanism && data.mechanism) {
|
||||||
|
data.exception.mechanism = data.mechanism;
|
||||||
|
delete data.mechanism;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.exception.mechanism = objectMerge$1(data.exception.mechanism || {}, {
|
||||||
|
type: 'generic',
|
||||||
|
handled: true
|
||||||
|
});
|
||||||
|
|
||||||
// Fire away!
|
// Fire away!
|
||||||
this._send(data);
|
this._send(data);
|
||||||
},
|
},
|
||||||
@@ -3394,7 +3576,7 @@ Raven.prototype = {
|
|||||||
|
|
||||||
if (this._hasNavigator && _navigator.userAgent) {
|
if (this._hasNavigator && _navigator.userAgent) {
|
||||||
httpData.headers = {
|
httpData.headers = {
|
||||||
'User-Agent': navigator.userAgent
|
'User-Agent': _navigator.userAgent
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3435,7 +3617,7 @@ Raven.prototype = {
|
|||||||
if (
|
if (
|
||||||
!last ||
|
!last ||
|
||||||
current.message !== last.message || // defined for captureMessage
|
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;
|
return false;
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1283
-1138
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1632
-1138
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+11
-4
@@ -13,6 +13,7 @@ const filter = require('gulp-filter');
|
|||||||
const sass = require('gulp-sass');
|
const sass = require('gulp-sass');
|
||||||
const cleancss = require('gulp-clean-css');
|
const cleancss = require('gulp-clean-css');
|
||||||
const run = require('run-sequence');
|
const run = require('run-sequence');
|
||||||
|
const header = require('gulp-header');
|
||||||
const prefix = require('gulp-autoprefixer');
|
const prefix = require('gulp-autoprefixer');
|
||||||
const gitbranch = require('git-branch');
|
const gitbranch = require('git-branch');
|
||||||
const svgstore = require('gulp-svgstore');
|
const svgstore = require('gulp-svgstore');
|
||||||
@@ -146,6 +147,7 @@ const build = {
|
|||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.pipe(header('typeof navigator === "object" && ')) // "Support" SSR (#935)
|
||||||
.pipe(sourcemaps.write(''))
|
.pipe(sourcemaps.write(''))
|
||||||
.pipe(gulp.dest(output))
|
.pipe(gulp.dest(output))
|
||||||
.pipe(filter('**/*.js'))
|
.pipe(filter('**/*.js'))
|
||||||
@@ -224,9 +226,14 @@ gulp.task('watch', () => {
|
|||||||
gulp.watch(paths.demo.src.sass, tasks.sass);
|
gulp.watch(paths.demo.src.sass, tasks.sass);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build distribution
|
||||||
|
gulp.task('build', () => {
|
||||||
|
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite);
|
||||||
|
});
|
||||||
|
|
||||||
// Default gulp task
|
// Default gulp task
|
||||||
gulp.task('default', () => {
|
gulp.task('default', () => {
|
||||||
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'watch');
|
run('build', 'watch');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish a version to CDN and demo
|
// Publish a version to CDN and demo
|
||||||
@@ -239,11 +246,11 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
const branch = {
|
const branch = {
|
||||||
current: gitbranch.sync(),
|
current: gitbranch.sync(),
|
||||||
master: 'master',
|
master: 'master',
|
||||||
beta: 'beta',
|
develop: 'develop',
|
||||||
};
|
};
|
||||||
const allowed = [
|
const allowed = [
|
||||||
branch.master,
|
branch.master,
|
||||||
branch.beta,
|
branch.develop,
|
||||||
];
|
];
|
||||||
|
|
||||||
const maxAge = 31536000; // 1 year
|
const maxAge = 31536000; // 1 year
|
||||||
@@ -255,7 +262,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
demo: {
|
demo: {
|
||||||
uploadPath: branch.current === branch.beta ? 'beta/' : null,
|
uploadPath: branch.current === branch.develop ? 'beta/' : null,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
|
||||||
Vary: 'Accept-Encoding',
|
Vary: 'Accept-Encoding',
|
||||||
|
|||||||
+14
-9
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "plyr",
|
"name": "plyr",
|
||||||
"version": "3.3.7",
|
"version": "3.3.12",
|
||||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||||
"homepage": "https://plyr.io",
|
"homepage": "https://plyr.io",
|
||||||
"main": "./dist/plyr.js",
|
"main": "./dist/plyr.js",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-eslint": "^8.2.3",
|
"babel-eslint": "^8.2.3",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.7.0",
|
||||||
"del": "^3.0.0",
|
"del": "^3.0.0",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-airbnb-base": "^12.1.0",
|
"eslint-config-airbnb-base": "^12.1.0",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-plugin-import": "^2.11.0",
|
"eslint-plugin-import": "^2.12.0",
|
||||||
"git-branch": "^2.0.1",
|
"git-branch": "^2.0.1",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-autoprefixer": "^5.0.0",
|
"gulp-autoprefixer": "^5.0.0",
|
||||||
@@ -24,24 +24,27 @@
|
|||||||
"gulp-clean-css": "^3.9.4",
|
"gulp-clean-css": "^3.9.4",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-filter": "^5.1.0",
|
"gulp-filter": "^5.1.0",
|
||||||
|
"gulp-header": "^2.0.5",
|
||||||
"gulp-open": "^3.0.1",
|
"gulp-open": "^3.0.1",
|
||||||
"gulp-rename": "^1.2.2",
|
"gulp-postcss": "^7.0.1",
|
||||||
"gulp-replace": "^0.6.1",
|
"gulp-rename": "^1.2.3",
|
||||||
|
"gulp-replace": "^1.0.0",
|
||||||
"gulp-s3": "^0.11.0",
|
"gulp-s3": "^0.11.0",
|
||||||
"gulp-sass": "^4.0.1",
|
"gulp-sass": "^4.0.1",
|
||||||
"gulp-size": "^3.0.0",
|
"gulp-size": "^3.0.0",
|
||||||
"gulp-sourcemaps": "^2.6.4",
|
"gulp-sourcemaps": "^2.6.4",
|
||||||
"gulp-svgmin": "^1.2.4",
|
"gulp-svgmin": "^1.2.4",
|
||||||
"gulp-svgstore": "^6.1.1",
|
"gulp-svgstore": "^6.1.1",
|
||||||
"gulp-uglify-es": "^1.0.1",
|
"gulp-uglify-es": "^1.0.4",
|
||||||
"gulp-util": "^3.0.8",
|
"gulp-util": "^3.0.8",
|
||||||
|
"postcss-custom-properties": "^7.0.0",
|
||||||
"prettier-eslint": "^8.8.1",
|
"prettier-eslint": "^8.8.1",
|
||||||
"prettier-stylelint": "^0.4.2",
|
"prettier-stylelint": "^0.4.2",
|
||||||
"rollup-plugin-babel": "^3.0.4",
|
"rollup-plugin-babel": "^3.0.4",
|
||||||
"rollup-plugin-commonjs": "^9.1.3",
|
"rollup-plugin-commonjs": "^9.1.3",
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
"rollup-plugin-node-resolve": "^3.3.0",
|
||||||
"run-sequence": "^2.2.1",
|
"run-sequence": "^2.2.1",
|
||||||
"stylelint": "^9.2.0",
|
"stylelint": "^9.2.1",
|
||||||
"stylelint-config-prettier": "^3.2.0",
|
"stylelint-config-prettier": "^3.2.0",
|
||||||
"stylelint-config-recommended": "^2.1.0",
|
"stylelint-config-recommended": "^2.1.0",
|
||||||
"stylelint-config-sass-guidelines": "^5.0.0",
|
"stylelint-config-sass-guidelines": "^5.0.0",
|
||||||
@@ -62,6 +65,8 @@
|
|||||||
"doc": "readme.md"
|
"doc": "readme.md"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "gulp build",
|
||||||
|
"lint": "eslint src/js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "Sam Potts <sam@potts.es>",
|
"author": "Sam Potts <sam@potts.es>",
|
||||||
@@ -69,7 +74,7 @@
|
|||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"custom-event-polyfill": "^0.3.0",
|
"custom-event-polyfill": "^0.3.0",
|
||||||
"loadjs": "^3.5.4",
|
"loadjs": "^3.5.4",
|
||||||
"npm": "^6.0.0",
|
"raven-js": "^3.26.1",
|
||||||
"raven-js": "^3.24.2"
|
"url-polyfill": "^1.0.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ Some awesome folks have made plugins for CMSs and Components for JavaScript fram
|
|||||||
|
|
||||||
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
|
Here's a quick run through on getting up and running. There's also a [demo on Codepen](http://codepen.io/sampotts/pen/jARJYp). You can grab all of the source with [NPM](https://www.npmjs.com/package/plyr) using `npm install plyr`.
|
||||||
|
|
||||||
|
### Try Plyr online
|
||||||
|
|
||||||
|
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=zaBgBy), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
|
||||||
|
|
||||||
### HTML
|
### HTML
|
||||||
|
|
||||||
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
|
Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
|
||||||
@@ -125,13 +129,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.
|
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
|
```html
|
||||||
<script src="https://cdn.plyr.io/3.3.7/plyr.js"></script>
|
<script src="https://cdn.plyr.io/3.3.12/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.12/plyr.polyfilled.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
### CSS
|
### CSS
|
||||||
|
|
||||||
@@ -144,13 +152,13 @@ Include the `plyr.css` stylsheet into your `<head>`
|
|||||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.7/plyr.css">
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.12/plyr.css">
|
||||||
```
|
```
|
||||||
|
|
||||||
### SVG Sprite
|
### SVG Sprite
|
||||||
|
|
||||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.7/plyr.svg`.
|
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.12/plyr.svg`.
|
||||||
|
|
||||||
## Ads
|
## Ads
|
||||||
|
|
||||||
@@ -299,7 +307,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
|||||||
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
|
| `invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter. |
|
||||||
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
||||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||||
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
|
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in unselectable language options). |
|
||||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
|
||||||
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
|
||||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||||
@@ -357,7 +365,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
|||||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
| `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. |
|
| `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. |
|
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||||
| `supports(type)` | String | Check support for a mime type. |
|
| `supports(type)` | String | Check support for a mime type. |
|
||||||
@@ -403,7 +411,8 @@ player.fullscreen.active; // false;
|
|||||||
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
|
||||||
| `poster` | ✓ | ✓ | 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. |
|
| `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. |
|
| `currentTrack` | ✓ | ✓ | Gets or sets the caption track by index. `-1` means the track is missing or captions is not active |
|
||||||
|
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. If your captions don't have any language data, or if you have multiple tracks with the same language, you may want to use `currentTrack` instead. |
|
||||||
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
|
||||||
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
||||||
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
|
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
|
||||||
@@ -603,17 +612,6 @@ document then the shortcuts will work when any element has focus, apart from an
|
|||||||
| `C` | Toggle captions |
|
| `C` | Toggle captions |
|
||||||
| `L` | Toggle loop |
|
| `L` | Toggle loop |
|
||||||
|
|
||||||
## Streaming
|
|
||||||
|
|
||||||
Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias
|
|
||||||
Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples:
|
|
||||||
|
|
||||||
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
|
|
||||||
* 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
|
||||||
|
|
||||||
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
|
||||||
|
|||||||
+170
-150
@@ -16,28 +16,6 @@ const captions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default language if not set
|
|
||||||
const stored = this.storage.get('language');
|
|
||||||
|
|
||||||
if (!utils.is.empty(stored)) {
|
|
||||||
this.captions.language = stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.is.empty(this.captions.language)) {
|
|
||||||
this.captions.language = this.config.captions.language.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set captions enabled state if not set
|
|
||||||
if (!utils.is.boolean(this.captions.active)) {
|
|
||||||
const active = this.storage.get('captions');
|
|
||||||
|
|
||||||
if (utils.is.boolean(active)) {
|
|
||||||
this.captions.active = active;
|
|
||||||
} else {
|
|
||||||
this.captions.active = this.config.captions.active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only Vimeo and HTML5 video supported at this point
|
// Only Vimeo and HTML5 video supported at this point
|
||||||
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
|
||||||
// Clear menu and hide
|
// Clear menu and hide
|
||||||
@@ -55,17 +33,6 @@ const captions = {
|
|||||||
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
utils.insertAfter(this.elements.captions, this.elements.wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the class hook
|
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
|
|
||||||
|
|
||||||
// Get tracks
|
|
||||||
const tracks = captions.getTracks.call(this);
|
|
||||||
|
|
||||||
// If no caption file exists, hide container for caption text
|
|
||||||
if (utils.is.empty(tracks)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get browser info
|
// Get browser info
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
@@ -94,82 +61,161 @@ const captions = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set language
|
// Try to load the value from storage
|
||||||
captions.setLanguage.call(this);
|
let active = this.storage.get('captions');
|
||||||
|
|
||||||
// Enable UI
|
// Otherwise fall back to the default config
|
||||||
captions.show.call(this);
|
if (!utils.is.boolean(active)) {
|
||||||
|
({ active } = this.config.captions);
|
||||||
|
}
|
||||||
|
|
||||||
// Set available languages in list
|
// Get language from storage, fallback to config
|
||||||
if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
|
let language = this.storage.get('language') || this.config.captions.language;
|
||||||
|
if (language === 'auto') {
|
||||||
|
[ language ] = (navigator.language || navigator.userLanguage).split('-');
|
||||||
|
}
|
||||||
|
// Set language and show if active
|
||||||
|
captions.setLanguage.call(this, language, active);
|
||||||
|
|
||||||
|
// Watch changes to textTracks and update captions menu
|
||||||
|
if (this.isHTML5) {
|
||||||
|
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
|
||||||
|
utils.on(this.media.textTracks, trackEvents, captions.update.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update available languages in list next tick (the event must not be triggered before the listeners)
|
||||||
|
setTimeout(captions.update.bind(this), 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const tracks = captions.getTracks.call(this, true);
|
||||||
|
// Get the wanted language
|
||||||
|
const { language, meta } = this.captions;
|
||||||
|
|
||||||
|
// Handle tracks (add event listener and "pseudo"-default)
|
||||||
|
if (this.isHTML5 && this.isVideo) {
|
||||||
|
tracks
|
||||||
|
.filter(track => !meta.get(track))
|
||||||
|
.forEach(track => {
|
||||||
|
this.debug.log('Track added', track);
|
||||||
|
// Attempt to store if the original dom element was "default"
|
||||||
|
meta.set(track, {
|
||||||
|
default: track.mode === 'showing',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Turn off native caption rendering to avoid double captions
|
||||||
|
track.mode = 'hidden';
|
||||||
|
|
||||||
|
// Add event listener for cue changes
|
||||||
|
utils.on(track, 'cuechange', () => captions.updateCues.call(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackRemoved = !tracks.find(track => track === this.captions.currentTrackNode);
|
||||||
|
const firstMatch = this.language !== language && tracks.find(track => track.language === language);
|
||||||
|
|
||||||
|
// Update language if removed or first matching track added
|
||||||
|
if (trackRemoved || firstMatch) {
|
||||||
|
captions.setLanguage.call(this, language, this.config.captions.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable or disable captions based on track length
|
||||||
|
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(tracks));
|
||||||
|
|
||||||
|
// Update available languages in list
|
||||||
|
if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
|
||||||
controls.setCaptionsMenu.call(this);
|
controls.setCaptionsMenu.call(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the captions language
|
set(index, setLanguage = true, show = true) {
|
||||||
setLanguage() {
|
const tracks = captions.getTracks.call(this);
|
||||||
// Setup HTML5 track rendering
|
|
||||||
if (this.isHTML5 && this.isVideo) {
|
|
||||||
captions.getTracks.call(this).forEach(track => {
|
|
||||||
// Show track
|
|
||||||
utils.on(track, 'cuechange', event => captions.setCue.call(this, event));
|
|
||||||
|
|
||||||
// Turn off native caption rendering to avoid double captions
|
// Disable captions if setting to -1
|
||||||
// eslint-disable-next-line
|
if (index === -1) {
|
||||||
track.mode = 'hidden';
|
this.toggleCaptions(false);
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get current track
|
if (!utils.is.number(index)) {
|
||||||
const currentTrack = captions.getCurrentTrack.call(this);
|
this.debug.warn('Invalid caption argument', index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if suported kind
|
if (!(index in tracks)) {
|
||||||
if (utils.is.track(currentTrack)) {
|
this.debug.warn('Track not found', index);
|
||||||
// If we change the active track while a cue is already displayed we need to update it
|
return;
|
||||||
if (Array.from(currentTrack.activeCues || []).length) {
|
}
|
||||||
captions.setCue.call(this, currentTrack);
|
|
||||||
}
|
if (this.captions.currentTrack !== index) {
|
||||||
|
this.captions.currentTrack = index;
|
||||||
|
const track = captions.getCurrentTrack.call(this);
|
||||||
|
const { language } = track || {};
|
||||||
|
|
||||||
|
// Store reference to node for invalidation on remove
|
||||||
|
this.captions.currentTrackNode = track;
|
||||||
|
|
||||||
|
// Prevent setting language in some cases, since it can violate user's intentions
|
||||||
|
if (setLanguage) {
|
||||||
|
this.captions.language = language;
|
||||||
}
|
}
|
||||||
} else if (this.isVimeo && this.captions.active) {
|
|
||||||
this.embed.enableTextTrack(this.language);
|
// Handle Vimeo captions
|
||||||
|
if (this.isVimeo) {
|
||||||
|
this.embed.enableTextTrack(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger event
|
||||||
|
utils.dispatchEvent.call(this, this.media, 'languagechange');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isHTML5 && this.isVideo) {
|
||||||
|
// If we change the active track while a cue is already displayed we need to update it
|
||||||
|
captions.updateCues.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show captions
|
||||||
|
if (show) {
|
||||||
|
this.toggleCaptions(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get the tracks
|
setLanguage(language, show = true) {
|
||||||
getTracks() {
|
if (!utils.is.string(language)) {
|
||||||
// Return empty array at least
|
this.debug.warn('Invalid language argument', language);
|
||||||
if (utils.is.nullOrUndefined(this.media)) {
|
return;
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
// Normalize
|
||||||
|
this.captions.language = language.toLowerCase();
|
||||||
|
|
||||||
// Only get accepted kinds
|
// Set currentTrack
|
||||||
return Array.from(this.media.textTracks || []).filter(track => [
|
const tracks = captions.getTracks.call(this);
|
||||||
'captions',
|
const track = captions.getCurrentTrack.call(this, true);
|
||||||
'subtitles',
|
captions.set.call(this, tracks.indexOf(track), false, show);
|
||||||
].includes(track.kind));
|
},
|
||||||
|
|
||||||
|
// Get current valid caption tracks
|
||||||
|
// If update is false it will also ignore tracks without metadata
|
||||||
|
// This is used to "freeze" the language options when captions.update is false
|
||||||
|
getTracks(update = false) {
|
||||||
|
// Handle media or textTracks missing or null
|
||||||
|
const tracks = Array.from((this.media || {}).textTracks || []);
|
||||||
|
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
|
||||||
|
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
|
||||||
|
return tracks
|
||||||
|
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
|
||||||
|
.filter(track => [
|
||||||
|
'captions',
|
||||||
|
'subtitles',
|
||||||
|
].includes(track.kind));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get the current track for the current language
|
// Get the current track for the current language
|
||||||
getCurrentTrack() {
|
getCurrentTrack(fromLanguage = false) {
|
||||||
const tracks = captions.getTracks.call(this);
|
const tracks = captions.getTracks.call(this);
|
||||||
|
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||||
if (!tracks.length) {
|
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||||
return null;
|
return (!fromLanguage && tracks[this.currentTrack]) || sorted.find(track => track.language === this.captions.language) || sorted[0];
|
||||||
}
|
|
||||||
|
|
||||||
// Get track based on current language
|
|
||||||
let track = tracks.find(track => track.language.toLowerCase() === this.language);
|
|
||||||
|
|
||||||
// Get the <track> with default attribute
|
|
||||||
if (!track) {
|
|
||||||
track = utils.getElement.call(this, 'track[default]');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first track
|
|
||||||
if (!track) {
|
|
||||||
[track] = tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
return track;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get UI label for track
|
// Get UI label for track
|
||||||
@@ -195,74 +241,48 @@ const captions = {
|
|||||||
return i18n.get('disabled', this.config);
|
return i18n.get('disabled', this.config);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Display active caption if it contains text
|
// Update captions using current track's active cues
|
||||||
setCue(input) {
|
// Also optional array argument in case there isn't any track (ex: vimeo)
|
||||||
// Get the track from the event if needed
|
updateCues(input) {
|
||||||
const track = utils.is.event(input) ? input.target : input;
|
|
||||||
const { activeCues } = track;
|
|
||||||
const active = activeCues.length && activeCues[0];
|
|
||||||
const currentTrack = captions.getCurrentTrack.call(this);
|
|
||||||
|
|
||||||
// Only display current track
|
|
||||||
if (track !== currentTrack) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a cue, if there is one
|
|
||||||
if (utils.is.cue(active)) {
|
|
||||||
captions.setText.call(this, active.getCueAsHTML());
|
|
||||||
} else {
|
|
||||||
captions.setText.call(this, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'cuechange');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Set the current caption
|
|
||||||
setText(input) {
|
|
||||||
// Requires UI
|
// Requires UI
|
||||||
if (!this.supported.ui) {
|
if (!this.supported.ui) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.element(this.elements.captions)) {
|
if (!utils.is.element(this.elements.captions)) {
|
||||||
const content = utils.createElement('span');
|
|
||||||
|
|
||||||
// Empty the container
|
|
||||||
utils.emptyElement(this.elements.captions);
|
|
||||||
|
|
||||||
// Default to empty
|
|
||||||
const caption = !utils.is.nullOrUndefined(input) ? input : '';
|
|
||||||
|
|
||||||
// Set the span content
|
|
||||||
if (utils.is.string(caption)) {
|
|
||||||
content.textContent = caption.trim();
|
|
||||||
} else {
|
|
||||||
content.appendChild(caption);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new caption text
|
|
||||||
this.elements.captions.appendChild(content);
|
|
||||||
} else {
|
|
||||||
this.debug.warn('No captions element to render to');
|
this.debug.warn('No captions element to render to');
|
||||||
}
|
return;
|
||||||
},
|
|
||||||
|
|
||||||
// Display captions container and button (for initialization)
|
|
||||||
show() {
|
|
||||||
// Try to load the value from storage
|
|
||||||
let active = this.storage.get('captions');
|
|
||||||
|
|
||||||
// Otherwise fall back to the default config
|
|
||||||
if (!utils.is.boolean(active)) {
|
|
||||||
({ active } = this.config.captions);
|
|
||||||
} else {
|
|
||||||
this.captions.active = active;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active) {
|
// Only accept array or empty input
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
|
if (!utils.is.nullOrUndefined(input) && !Array.isArray(input)) {
|
||||||
utils.toggleState(this.elements.buttons.captions, true);
|
this.debug.warn('updateCues: Invalid input', input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cues = input;
|
||||||
|
|
||||||
|
// Get cues from track
|
||||||
|
if (!cues) {
|
||||||
|
const track = captions.getCurrentTrack.call(this);
|
||||||
|
cues = Array.from((track || {}).activeCues || [])
|
||||||
|
.map(cue => cue.getCueAsHTML())
|
||||||
|
.map(utils.getHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new caption text
|
||||||
|
const content = cues.map(cueText => cueText.trim()).join('\n');
|
||||||
|
const changed = content !== this.elements.captions.innerHTML;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
// Empty the container and create a new child element
|
||||||
|
utils.emptyElement(this.elements.captions);
|
||||||
|
const caption = utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.caption));
|
||||||
|
caption.innerHTML = content;
|
||||||
|
this.elements.captions.appendChild(caption);
|
||||||
|
|
||||||
|
// Trigger event
|
||||||
|
utils.dispatchEvent.call(this, this.media, 'cuechange');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+203
-87
@@ -6,34 +6,13 @@ import captions from './captions';
|
|||||||
import html5 from './html5';
|
import html5 from './html5';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import ui from './ui';
|
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
|
|
||||||
// Sniff out the browser
|
// Sniff out the browser
|
||||||
const browser = utils.getBrowser();
|
const browser = utils.getBrowser();
|
||||||
|
|
||||||
const controls = {
|
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
|
// Get icon URL
|
||||||
getIconUrl() {
|
getIconUrl() {
|
||||||
@@ -373,7 +352,7 @@ const controls = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.textContent = `% ${suffix.toLowerCase()}`;
|
progress.innerText = `% ${suffix.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.elements.display[type] = progress;
|
this.elements.display[type] = progress;
|
||||||
@@ -397,7 +376,7 @@ const controls = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Create a settings menu item
|
// Create a settings menu item
|
||||||
createMenuItem(value, list, type, title, badge = null, checked = false) {
|
createMenuItem({value, list, type, title, badge = null, checked = false}) {
|
||||||
const item = utils.createElement('li');
|
const item = utils.createElement('li');
|
||||||
|
|
||||||
const label = utils.createElement('label', {
|
const label = utils.createElement('label', {
|
||||||
@@ -429,6 +408,124 @@ const controls = {
|
|||||||
list.appendChild(item);
|
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
|
// Update hover tooltip for seeking
|
||||||
updateSeekTooltip(event) {
|
updateSeekTooltip(event) {
|
||||||
// Bail if setting not true
|
// Bail if setting not true
|
||||||
@@ -443,7 +540,7 @@ const controls = {
|
|||||||
|
|
||||||
// Calculate percentage
|
// Calculate percentage
|
||||||
let percent = 0;
|
let percent = 0;
|
||||||
const clientRect = this.elements.inputs.seek.getBoundingClientRect();
|
const clientRect = this.elements.progress.getBoundingClientRect();
|
||||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||||
|
|
||||||
const toggle = toggle => {
|
const toggle = toggle => {
|
||||||
@@ -473,7 +570,7 @@ const controls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display the time a click would seek to
|
// 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
|
// Set position
|
||||||
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
||||||
@@ -488,6 +585,47 @@ 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
|
// Hide/show a tab
|
||||||
toggleTab(setting, toggle) {
|
toggleTab(setting, toggle) {
|
||||||
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||||
@@ -526,27 +664,7 @@ const controls = {
|
|||||||
|
|
||||||
// Get the badge HTML for HD, 4K etc
|
// Get the badge HTML for HD, 4K etc
|
||||||
const getBadge = quality => {
|
const getBadge = quality => {
|
||||||
let label = '';
|
const label = i18n.get(`qualityBadge.${quality}`, this.config);
|
||||||
|
|
||||||
switch (quality) {
|
|
||||||
case 2160:
|
|
||||||
label = '4K';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1440:
|
|
||||||
case 1080:
|
|
||||||
case 720:
|
|
||||||
label = 'HD';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 576:
|
|
||||||
case 480:
|
|
||||||
label = 'SD';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!label.length) {
|
if (!label.length) {
|
||||||
return null;
|
return null;
|
||||||
@@ -562,15 +680,19 @@ const controls = {
|
|||||||
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
|
||||||
})
|
})
|
||||||
.forEach(quality => {
|
.forEach(quality => {
|
||||||
const label = controls.getLabel.call(this, 'quality', quality);
|
controls.createMenuItem.call(this, {
|
||||||
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
|
value: quality,
|
||||||
|
list,
|
||||||
|
type,
|
||||||
|
title: controls.getLabel.call(this, 'quality', quality),
|
||||||
|
badge: getBadge(quality),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Translate a value into a nice label
|
// Translate a value into a nice label
|
||||||
// TODO: Localisation
|
|
||||||
getLabel(setting, value) {
|
getLabel(setting, value) {
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'speed':
|
case 'speed':
|
||||||
@@ -578,7 +700,13 @@ const controls = {
|
|||||||
|
|
||||||
case 'quality':
|
case 'quality':
|
||||||
if (utils.is.number(value)) {
|
if (utils.is.number(value)) {
|
||||||
return `${value}p`;
|
const label = i18n.get(`qualityLabel.${value}`, this.config);
|
||||||
|
|
||||||
|
if (!label.length) {
|
||||||
|
return `${value}p`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.toTitleCase(value);
|
return utils.toTitleCase(value);
|
||||||
@@ -599,16 +727,7 @@ const controls = {
|
|||||||
|
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'captions':
|
case 'captions':
|
||||||
if (this.captions.active) {
|
value = this.currentTrack;
|
||||||
if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) {
|
|
||||||
value = this.captions.language;
|
|
||||||
} else {
|
|
||||||
value = 'enabled';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -708,10 +827,10 @@ const controls = {
|
|||||||
// TODO: Captions or language? Currently it's mixed
|
// TODO: Captions or language? Currently it's mixed
|
||||||
const type = 'captions';
|
const type = 'captions';
|
||||||
const list = this.elements.settings.panes.captions.querySelector('ul');
|
const list = this.elements.settings.panes.captions.querySelector('ul');
|
||||||
|
const tracks = captions.getTracks.call(this);
|
||||||
|
|
||||||
// Toggle the pane and tab
|
// Toggle the pane and tab
|
||||||
const toggle = captions.getTracks.call(this).length;
|
controls.toggleTab.call(this, type, tracks.length);
|
||||||
controls.toggleTab.call(this, type, toggle);
|
|
||||||
|
|
||||||
// Empty the menu
|
// Empty the menu
|
||||||
utils.emptyElement(list);
|
utils.emptyElement(list);
|
||||||
@@ -720,37 +839,31 @@ const controls = {
|
|||||||
controls.checkMenu.call(this);
|
controls.checkMenu.call(this);
|
||||||
|
|
||||||
// If there's no captions, bail
|
// If there's no captions, bail
|
||||||
if (!toggle) {
|
if (!tracks.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-map the tracks into just the data we need
|
// Generate options data
|
||||||
const tracks = captions.getTracks.call(this).map(track => ({
|
const options = tracks.map((track, value) => ({
|
||||||
language: !utils.is.empty(track.language) ? track.language : 'enabled',
|
value,
|
||||||
label: captions.getLabel.call(this, track),
|
checked: this.captions.active && this.currentTrack === value,
|
||||||
|
title: captions.getLabel.call(this, track),
|
||||||
|
badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
|
||||||
|
list,
|
||||||
|
type: 'language',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add the "Disabled" option to turn off captions
|
// Add the "Disabled" option to turn off captions
|
||||||
tracks.unshift({
|
options.unshift({
|
||||||
language: '',
|
value: -1,
|
||||||
label: i18n.get('disabled', this.config),
|
checked: !this.captions.active,
|
||||||
|
title: i18n.get('disabled', this.config),
|
||||||
|
list,
|
||||||
|
type: 'language',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate options
|
// Generate options
|
||||||
tracks.forEach(track => {
|
options.forEach(controls.createMenuItem.bind(this));
|
||||||
controls.createMenuItem.call(
|
|
||||||
this,
|
|
||||||
track.language,
|
|
||||||
list,
|
|
||||||
'language',
|
|
||||||
track.label,
|
|
||||||
track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null,
|
|
||||||
track.language.toLowerCase() === this.captions.language.toLowerCase(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store reference
|
|
||||||
this.options.captions = tracks.map(track => track.language);
|
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
},
|
},
|
||||||
@@ -807,8 +920,12 @@ const controls = {
|
|||||||
|
|
||||||
// Create items
|
// Create items
|
||||||
this.options.speed.forEach(speed => {
|
this.options.speed.forEach(speed => {
|
||||||
const label = controls.getLabel.call(this, 'speed', speed);
|
controls.createMenuItem.call(this, {
|
||||||
controls.createMenuItem.call(this, speed, list, type, label);
|
value: speed,
|
||||||
|
list,
|
||||||
|
type,
|
||||||
|
title: controls.getLabel.call(this, 'speed', speed),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
controls.updateSetting.call(this, type, list);
|
controls.updateSetting.call(this, type, list);
|
||||||
@@ -1027,7 +1144,6 @@ const controls = {
|
|||||||
const tooltip = utils.createElement(
|
const tooltip = utils.createElement(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
role: 'tooltip',
|
|
||||||
class: this.config.classNames.tooltip,
|
class: this.config.classNames.tooltip,
|
||||||
},
|
},
|
||||||
'00:00',
|
'00:00',
|
||||||
|
|||||||
+17
-7
@@ -56,7 +56,7 @@ const defaults = {
|
|||||||
// Sprite (for icons)
|
// Sprite (for icons)
|
||||||
loadSprite: true,
|
loadSprite: true,
|
||||||
iconPrefix: 'plyr',
|
iconPrefix: 'plyr',
|
||||||
iconUrl: 'https://cdn.plyr.io/3.3.7/plyr.svg',
|
iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg',
|
||||||
|
|
||||||
// Blank video (used to prevent errors on source change)
|
// Blank video (used to prevent errors on source change)
|
||||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||||
@@ -115,7 +115,10 @@ const defaults = {
|
|||||||
// Captions settings
|
// Captions settings
|
||||||
captions: {
|
captions: {
|
||||||
active: false,
|
active: false,
|
||||||
language: (navigator.language || navigator.userLanguage).split('-')[0],
|
language: 'auto',
|
||||||
|
// Listen to new tracks added after Plyr is initialized.
|
||||||
|
// This is needed for streaming captions, but may result in unselectable options
|
||||||
|
update: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fullscreen settings
|
// Fullscreen settings
|
||||||
@@ -187,6 +190,14 @@ const defaults = {
|
|||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
advertisement: 'Ad',
|
advertisement: 'Ad',
|
||||||
|
qualityBadge: {
|
||||||
|
2160: '4K',
|
||||||
|
1440: 'HD',
|
||||||
|
1080: 'HD',
|
||||||
|
720: 'HD',
|
||||||
|
576: 'SD',
|
||||||
|
480: 'SD',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// URLs
|
// URLs
|
||||||
@@ -199,7 +210,6 @@ const defaults = {
|
|||||||
youtube: {
|
youtube: {
|
||||||
sdk: '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',
|
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||||
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
|
|
||||||
},
|
},
|
||||||
googleIMA: {
|
googleIMA: {
|
||||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||||
@@ -312,13 +322,13 @@ const defaults = {
|
|||||||
display: {
|
display: {
|
||||||
currentTime: '.plyr__time--current',
|
currentTime: '.plyr__time--current',
|
||||||
duration: '.plyr__time--duration',
|
duration: '.plyr__time--duration',
|
||||||
buffer: '.plyr__progress--buffer',
|
buffer: '.plyr__progress__buffer',
|
||||||
played: '.plyr__progress--played',
|
loop: '.plyr__progress__loop', // Used later
|
||||||
loop: '.plyr__progress--loop',
|
|
||||||
volume: '.plyr__volume--display',
|
volume: '.plyr__volume--display',
|
||||||
},
|
},
|
||||||
progress: '.plyr__progress',
|
progress: '.plyr__progress',
|
||||||
captions: '.plyr__captions',
|
captions: '.plyr__captions',
|
||||||
|
caption: '.plyr__caption',
|
||||||
menu: {
|
menu: {
|
||||||
quality: '.js-plyr__menu__list--quality',
|
quality: '.js-plyr__menu__list--quality',
|
||||||
},
|
},
|
||||||
@@ -332,13 +342,13 @@ const defaults = {
|
|||||||
embed: 'plyr__video-embed',
|
embed: 'plyr__video-embed',
|
||||||
embedContainer: 'plyr__video-embed__container',
|
embedContainer: 'plyr__video-embed__container',
|
||||||
poster: 'plyr__poster',
|
poster: 'plyr__poster',
|
||||||
|
posterEnabled: 'plyr__poster-enabled',
|
||||||
ads: 'plyr__ads',
|
ads: 'plyr__ads',
|
||||||
control: 'plyr__control',
|
control: 'plyr__control',
|
||||||
playing: 'plyr--playing',
|
playing: 'plyr--playing',
|
||||||
paused: 'plyr--paused',
|
paused: 'plyr--paused',
|
||||||
stopped: 'plyr--stopped',
|
stopped: 'plyr--stopped',
|
||||||
loading: 'plyr--loading',
|
loading: 'plyr--loading',
|
||||||
error: 'plyr--has-error',
|
|
||||||
hover: 'plyr--hover',
|
hover: 'plyr--hover',
|
||||||
tooltip: 'plyr__tooltip',
|
tooltip: 'plyr__tooltip',
|
||||||
cues: 'plyr__cues',
|
cues: 'plyr__cues',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function onChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger an event
|
// 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
|
// Trap focus in container
|
||||||
if (!browser.isIos) {
|
if (!browser.isIos) {
|
||||||
|
|||||||
+7
-3
@@ -99,6 +99,13 @@ const html5 = {
|
|||||||
// Set new source
|
// Set new source
|
||||||
player.media.src = supported[0].getAttribute('src');
|
player.media.src = supported[0].getAttribute('src');
|
||||||
|
|
||||||
|
// Restore time
|
||||||
|
const onLoadedMetaData = () => {
|
||||||
|
player.currentTime = currentTime;
|
||||||
|
player.off('loadedmetadata', onLoadedMetaData);
|
||||||
|
};
|
||||||
|
player.on('loadedmetadata', onLoadedMetaData);
|
||||||
|
|
||||||
// Load new source
|
// Load new source
|
||||||
player.media.load();
|
player.media.load();
|
||||||
|
|
||||||
@@ -107,9 +114,6 @@ const html5 = {
|
|||||||
player.play();
|
player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore time
|
|
||||||
player.currentTime = currentTime;
|
|
||||||
|
|
||||||
// Trigger change event
|
// Trigger change event
|
||||||
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
|
||||||
quality: input,
|
quality: input,
|
||||||
|
|||||||
+6
-2
@@ -6,11 +6,15 @@ import utils from './utils';
|
|||||||
|
|
||||||
const i18n = {
|
const i18n = {
|
||||||
get(key = '', config = {}) {
|
get(key = '', config = {}) {
|
||||||
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
|
if (utils.is.empty(key) || utils.is.empty(config)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let string = config.i18n[key];
|
let string = utils.getDeep(config.i18n, key);
|
||||||
|
|
||||||
|
if (utils.is.empty(string)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const replace = {
|
const replace = {
|
||||||
'{seektime}': config.seekTime,
|
'{seektime}': config.seekTime,
|
||||||
|
|||||||
+128
-37
@@ -74,7 +74,10 @@ class Listeners {
|
|||||||
// and if the focused element is not editable (e.g. text input)
|
// and if the focused element is not editable (e.g. text input)
|
||||||
// and any that accept key input http://webaim.org/techniques/keyboard/
|
// and any that accept key input http://webaim.org/techniques/keyboard/
|
||||||
const focused = utils.getFocusElement();
|
const focused = utils.getFocusElement();
|
||||||
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
|
if (utils.is.element(focused) && (
|
||||||
|
focused !== this.player.elements.inputs.seek &&
|
||||||
|
utils.matches(focused, this.player.config.selectors.editable))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,22 +241,45 @@ class Listeners {
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
// Toggle controls on mouse events and entering fullscreen
|
||||||
if (this.player.config.hideControls) {
|
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||||
// Toggle controls on mouse events and entering fullscreen
|
const { controls } = this.player.elements;
|
||||||
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
|
||||||
this.player.toggleControls(event);
|
// 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
|
// Listen for media events
|
||||||
media() {
|
media() {
|
||||||
// Time change on 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
|
// Display duration
|
||||||
utils.on(this.player.media, 'durationchange loadeddata 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
|
// Check for audio tracks on load
|
||||||
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
|
||||||
@@ -272,10 +298,10 @@ class Listeners {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check for buffer progress
|
// 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
|
// 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
|
// Handle play/pause
|
||||||
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', 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));
|
||||||
@@ -283,9 +309,6 @@ class Listeners {
|
|||||||
// Loading state
|
// Loading state
|
||||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
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
|
// 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
|
// 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', () => {
|
utils.on(this.player.media, 'playing', () => {
|
||||||
@@ -391,7 +414,7 @@ class Listeners {
|
|||||||
'keyup',
|
'keyup',
|
||||||
'keydown',
|
'keydown',
|
||||||
]).join(' '), event => {
|
]).join(' '), event => {
|
||||||
let detail = {};
|
let {detail = {}} = event;
|
||||||
|
|
||||||
// Get error details from media
|
// Get error details from media
|
||||||
if (event.type === 'error') {
|
if (event.type === 'error') {
|
||||||
@@ -500,7 +523,7 @@ class Listeners {
|
|||||||
proxy(
|
proxy(
|
||||||
event,
|
event,
|
||||||
() => {
|
() => {
|
||||||
this.player.language = event.target.value;
|
this.player.currentTrack = Number(event.target.value);
|
||||||
showHomeTab();
|
showHomeTab();
|
||||||
},
|
},
|
||||||
'language',
|
'language',
|
||||||
@@ -529,12 +552,60 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const code = event.keyCode ? event.keyCode : event.which;
|
||||||
|
const eventType = event.type;
|
||||||
|
|
||||||
|
if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Seek
|
// Seek
|
||||||
on(
|
on(
|
||||||
this.player.elements.inputs.seek,
|
this.player.elements.inputs.seek,
|
||||||
inputEvent,
|
inputEvent,
|
||||||
event => {
|
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',
|
'seek',
|
||||||
);
|
);
|
||||||
@@ -549,7 +620,8 @@ class Listeners {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.player.config.invertTime = !this.player.config.invertTime;
|
this.player.config.invertTime = !this.player.config.invertTime;
|
||||||
ui.timeUpdate.call(this.player);
|
|
||||||
|
controls.timeUpdate.call(this.player);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,26 +645,45 @@ class Listeners {
|
|||||||
// Seek tooltip
|
// Seek tooltip
|
||||||
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
||||||
|
|
||||||
// Toggle controls visibility based on mouse movement
|
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
if (this.player.config.hideControls) {
|
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||||
// Watch for cursor over controls so they don't hide when trying to interact
|
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||||
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
|
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||||
this.player.elements.controls.pressed = [
|
this.player.elements.controls.pressed = [
|
||||||
'mousedown',
|
'mousedown',
|
||||||
'touchstart',
|
'touchstart',
|
||||||
].includes(event.type);
|
].includes(event.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus in/out on controls
|
// Focus in/out on controls
|
||||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||||
this.player.toggleControls(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
|
// Mouse wheel for volume
|
||||||
on(
|
on(
|
||||||
|
|||||||
+48
-43
@@ -7,6 +7,17 @@ import controls from './../controls';
|
|||||||
import ui from './../ui';
|
import ui from './../ui';
|
||||||
import utils from './../utils';
|
import utils from './../utils';
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (play && !this.embed.hasPlayed) {
|
||||||
|
this.embed.hasPlayed = true;
|
||||||
|
}
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const vimeo = {
|
const vimeo = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
@@ -99,11 +110,8 @@ const vimeo = {
|
|||||||
// Get original image
|
// Get original image
|
||||||
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
|
||||||
|
|
||||||
// Set attribute
|
// Set and show poster
|
||||||
player.media.setAttribute('poster', url.href);
|
ui.setPoster.call(player, url.href);
|
||||||
|
|
||||||
// Update
|
|
||||||
ui.setPoster.call(player);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup instance
|
// Setup instance
|
||||||
@@ -123,15 +131,13 @@ const vimeo = {
|
|||||||
|
|
||||||
// Create a faux HTML5 API using the Vimeo API
|
// Create a faux HTML5 API using the Vimeo API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
player.embed.play().then(() => {
|
assurePlaybackState.call(player, true);
|
||||||
player.media.paused = false;
|
return player.embed.play();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
player.embed.pause().then(() => {
|
assurePlaybackState.call(player, false);
|
||||||
player.media.paused = true;
|
return player.embed.pause();
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.stop = () => {
|
player.media.stop = () => {
|
||||||
@@ -146,25 +152,27 @@ const vimeo = {
|
|||||||
return currentTime;
|
return currentTime;
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Get current paused state
|
// Vimeo will automatically play on seek if the video hasn't been played before
|
||||||
// Vimeo will automatically play on seek
|
|
||||||
const { paused } = player.media;
|
|
||||||
|
|
||||||
// Set seeking flag
|
// Get current paused state and volume etc
|
||||||
player.media.seeking = true;
|
const { embed, media, paused, volume } = player;
|
||||||
|
const restorePause = paused && !embed.hasPlayed;
|
||||||
|
|
||||||
// Trigger seeking
|
// Set seeking state and trigger event
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
media.seeking = true;
|
||||||
|
utils.dispatchEvent.call(player, media, 'seeking');
|
||||||
|
|
||||||
// Seek after events
|
// If paused, mute until seek is complete
|
||||||
player.embed.setCurrentTime(time).catch(() => {
|
Promise.resolve(restorePause && embed.setVolume(0))
|
||||||
// Do nothing
|
// Seek
|
||||||
});
|
.then(() => embed.setCurrentTime(time))
|
||||||
|
// Restore paused
|
||||||
// Restore pause state
|
.then(() => restorePause && embed.pause())
|
||||||
if (paused) {
|
// Restore volume
|
||||||
player.pause();
|
.then(() => restorePause && embed.setVolume(volume))
|
||||||
}
|
.catch(() => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -297,17 +305,20 @@ const vimeo = {
|
|||||||
captions.setup.call(player);
|
captions.setup.call(player);
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('cuechange', data => {
|
player.embed.on('cuechange', ({ cues = [] }) => {
|
||||||
let cue = null;
|
const strippedCues = cues.map(cue => utils.stripHTML(cue.text));
|
||||||
|
captions.updateCues.call(player, strippedCues);
|
||||||
if (data.cues.length) {
|
|
||||||
cue = utils.stripHTML(data.cues[0].text);
|
|
||||||
}
|
|
||||||
|
|
||||||
captions.setText.call(player, cue);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('loaded', () => {
|
player.embed.on('loaded', () => {
|
||||||
|
// Assure state and events are updated on autoplay
|
||||||
|
player.embed.getPaused().then(paused => {
|
||||||
|
assurePlaybackState.call(player, !paused);
|
||||||
|
if (!paused) {
|
||||||
|
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (utils.is.element(player.embed.element) && player.supported.ui) {
|
if (utils.is.element(player.embed.element) && player.supported.ui) {
|
||||||
const frame = player.embed.element;
|
const frame = player.embed.element;
|
||||||
|
|
||||||
@@ -318,17 +329,12 @@ const vimeo = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('play', () => {
|
player.embed.on('play', () => {
|
||||||
// Only fire play if paused before
|
assurePlaybackState.call(player, true);
|
||||||
if (player.media.paused) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
}
|
|
||||||
player.media.paused = false;
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'playing');
|
utils.dispatchEvent.call(player, player.media, 'playing');
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('pause', () => {
|
player.embed.on('pause', () => {
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('timeupdate', data => {
|
player.embed.on('timeupdate', data => {
|
||||||
@@ -359,7 +365,6 @@ const vimeo = {
|
|||||||
player.embed.on('seeked', () => {
|
player.embed.on('seeked', () => {
|
||||||
player.media.seeking = false;
|
player.media.seeking = false;
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
utils.dispatchEvent.call(player, player.media, 'seeked');
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
player.embed.on('ended', () => {
|
player.embed.on('ended', () => {
|
||||||
|
|||||||
+71
-43
@@ -64,6 +64,17 @@ function mapQualityUnits(levels) {
|
|||||||
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set playback state and trigger change (only on actual change)
|
||||||
|
function assurePlaybackState(play) {
|
||||||
|
if (play && !this.embed.hasPlayed) {
|
||||||
|
this.embed.hasPlayed = true;
|
||||||
|
}
|
||||||
|
if (this.media.paused === play) {
|
||||||
|
this.media.paused = !play;
|
||||||
|
utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const youtube = {
|
const youtube = {
|
||||||
setup() {
|
setup() {
|
||||||
// Add embed class for responsive
|
// Add embed class for responsive
|
||||||
@@ -162,7 +173,19 @@ const youtube = {
|
|||||||
player.media = utils.replaceElement(container, player.media);
|
player.media = utils.replaceElement(container, player.media);
|
||||||
|
|
||||||
// Set poster image
|
// Set poster image
|
||||||
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
|
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
|
// Setup instance
|
||||||
// https://developers.google.com/youtube/iframe_api_reference
|
// https://developers.google.com/youtube/iframe_api_reference
|
||||||
@@ -252,10 +275,12 @@ const youtube = {
|
|||||||
|
|
||||||
// Create a faux HTML5 API using the YouTube API
|
// Create a faux HTML5 API using the YouTube API
|
||||||
player.media.play = () => {
|
player.media.play = () => {
|
||||||
|
assurePlaybackState.call(player, true);
|
||||||
instance.playVideo();
|
instance.playVideo();
|
||||||
};
|
};
|
||||||
|
|
||||||
player.media.pause = () => {
|
player.media.pause = () => {
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
instance.pauseVideo();
|
instance.pauseVideo();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,22 +298,17 @@ const youtube = {
|
|||||||
return Number(instance.getCurrentTime());
|
return Number(instance.getCurrentTime());
|
||||||
},
|
},
|
||||||
set(time) {
|
set(time) {
|
||||||
// Vimeo will automatically play on seek
|
// If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
|
||||||
const { paused } = player.media;
|
if (player.paused) {
|
||||||
|
player.embed.mute();
|
||||||
|
}
|
||||||
|
|
||||||
// Set seeking flag
|
// Set seeking state and trigger event
|
||||||
player.media.seeking = true;
|
player.media.seeking = true;
|
||||||
|
|
||||||
// Trigger seeking
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeking');
|
utils.dispatchEvent.call(player, player.media, 'seeking');
|
||||||
|
|
||||||
// Seek after events sent
|
// Seek after events sent
|
||||||
instance.seekTo(time);
|
instance.seekTo(time);
|
||||||
|
|
||||||
// Restore pause state
|
|
||||||
if (paused) {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -407,6 +427,17 @@ const youtube = {
|
|||||||
// Reset timer
|
// Reset timer
|
||||||
clearInterval(player.timers.playing);
|
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
|
// Handle events
|
||||||
// -1 Unstarted
|
// -1 Unstarted
|
||||||
// 0 Ended
|
// 0 Ended
|
||||||
@@ -426,7 +457,7 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 0:
|
case 0:
|
||||||
player.media.paused = true;
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
// YouTube doesn't support loop for a single video, so mimick it.
|
// YouTube doesn't support loop for a single video, so mimick it.
|
||||||
if (player.media.loop) {
|
if (player.media.loop) {
|
||||||
@@ -440,42 +471,39 @@ const youtube = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
case 1:
|
||||||
// If we were seeking, fire seeked event
|
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
if (player.media.seeking) {
|
if (player.media.paused && !player.embed.hasPlayed) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'seeked');
|
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.seeking = false;
|
|
||||||
|
|
||||||
// Only fire play if paused before
|
|
||||||
if (player.media.paused) {
|
|
||||||
utils.dispatchEvent.call(player, player.media, 'play');
|
|
||||||
}
|
|
||||||
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;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
player.media.paused = true;
|
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
|
||||||
|
if (!player.muted) {
|
||||||
utils.dispatchEvent.call(player, player.media, 'pause');
|
player.embed.unMute();
|
||||||
|
}
|
||||||
|
assurePlaybackState.call(player, false);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
+66
-189
@@ -1,6 +1,6 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr
|
// Plyr
|
||||||
// plyr.js v3.3.7
|
// plyr.js v3.3.12
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -55,6 +55,7 @@ class Plyr {
|
|||||||
this.config = utils.extend(
|
this.config = utils.extend(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
|
Plyr.defaults,
|
||||||
options || {},
|
options || {},
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +84,8 @@ class Plyr {
|
|||||||
// Captions
|
// Captions
|
||||||
this.captions = {
|
this.captions = {
|
||||||
active: null,
|
active: null,
|
||||||
currentTrack: null,
|
currentTrack: -1,
|
||||||
|
meta: new WeakMap(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fullscreen
|
// Fullscreen
|
||||||
@@ -95,7 +97,6 @@ class Plyr {
|
|||||||
this.options = {
|
this.options = {
|
||||||
speed: [],
|
speed: [],
|
||||||
quality: [],
|
quality: [],
|
||||||
captions: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
@@ -431,21 +432,16 @@ class Plyr {
|
|||||||
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
* @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
|
||||||
*/
|
*/
|
||||||
set currentTime(input) {
|
set currentTime(input) {
|
||||||
let targetTime = 0;
|
// Bail if media duration isn't available yet
|
||||||
|
if (!this.duration) {
|
||||||
if (utils.is.number(input)) {
|
return;
|
||||||
targetTime = input;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalise targetTime
|
// Validate input
|
||||||
if (targetTime < 0) {
|
const inputIsValid = utils.is.number(input) && input > 0;
|
||||||
targetTime = 0;
|
|
||||||
} else if (targetTime > this.duration) {
|
|
||||||
targetTime = this.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set
|
// Set
|
||||||
this.media.currentTime = targetTime;
|
this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
this.debug.log(`Seeking to ${this.currentTime} seconds`);
|
||||||
@@ -493,11 +489,11 @@ class Plyr {
|
|||||||
// Faux duration set via config
|
// Faux duration set via config
|
||||||
const fauxDuration = parseFloat(this.config.duration);
|
const fauxDuration = parseFloat(this.config.duration);
|
||||||
|
|
||||||
// True duration
|
// Media duration can be NaN before the media has loaded
|
||||||
const realDuration = this.media ? Number(this.media.duration) : 0;
|
const duration = (this.media || {}).duration || 0;
|
||||||
|
|
||||||
// If custom duration is funky, use regular duration
|
// If config duration is funky, use regular duration
|
||||||
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
|
return fauxDuration || duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -679,7 +675,7 @@ class Plyr {
|
|||||||
quality = Number(input);
|
quality = Number(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!utils.is.number(quality) || quality === 0) {
|
if (!utils.is.number(quality)) {
|
||||||
quality = this.storage.get('quality');
|
quality = this.storage.get('quality');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,10 +797,7 @@ class Plyr {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.string(input)) {
|
ui.setPoster.call(this, input);
|
||||||
this.media.setAttribute('poster', input);
|
|
||||||
ui.setPoster.call(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -845,82 +838,51 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the method is called without parameter, toggle based on current value
|
// If the method is called without parameter, toggle based on current value
|
||||||
const show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
const active = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
|
||||||
|
|
||||||
// Nothing to change...
|
|
||||||
if (this.captions.active === show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set global
|
|
||||||
this.captions.active = show;
|
|
||||||
|
|
||||||
// Toggle state
|
// Toggle state
|
||||||
utils.toggleState(this.elements.buttons.captions, this.captions.active);
|
utils.toggleState(this.elements.buttons.captions, active);
|
||||||
|
|
||||||
// Add class hook
|
// Add class hook
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active);
|
utils.toggleClass(this.elements.container, this.config.classNames.captions.active, active);
|
||||||
|
|
||||||
// Trigger an event
|
// Update state and trigger event
|
||||||
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
if (active !== this.captions.active) {
|
||||||
|
this.captions.active = active;
|
||||||
|
utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the captions language
|
* Set the caption track by index
|
||||||
|
* @param {number} - Caption index
|
||||||
|
*/
|
||||||
|
set currentTrack(input) {
|
||||||
|
captions.set.call(this, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current caption track index (-1 if disabled)
|
||||||
|
*/
|
||||||
|
get currentTrack() {
|
||||||
|
const { active, currentTrack } = this.captions;
|
||||||
|
return active ? currentTrack : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the wanted language for captions
|
||||||
|
* Since tracks can be added later it won't update the actual caption track until there is a matching track
|
||||||
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
* @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||||
*/
|
*/
|
||||||
set language(input) {
|
set language(input) {
|
||||||
// Nothing specified
|
captions.setLanguage.call(this, input);
|
||||||
if (!utils.is.string(input)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If empty string is passed, assume disable captions
|
|
||||||
if (utils.is.empty(input)) {
|
|
||||||
this.toggleCaptions(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize
|
|
||||||
const language = input.toLowerCase();
|
|
||||||
|
|
||||||
// Check for support
|
|
||||||
if (!this.options.captions.includes(language)) {
|
|
||||||
this.debug.warn(`Unsupported language option: ${language}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure captions are enabled
|
|
||||||
this.toggleCaptions(true);
|
|
||||||
|
|
||||||
// Enabled only
|
|
||||||
if (language === 'enabled') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing to change, bail
|
|
||||||
if (this.language === language) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update config
|
|
||||||
this.captions.language = language;
|
|
||||||
|
|
||||||
// Clear caption
|
|
||||||
captions.setText.call(this, null);
|
|
||||||
|
|
||||||
// Update captions
|
|
||||||
captions.setLanguage.call(this);
|
|
||||||
|
|
||||||
// Trigger an event
|
|
||||||
utils.dispatchEvent.call(this, this.media, 'languagechange');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current captions language
|
* Get the current track's language
|
||||||
*/
|
*/
|
||||||
get language() {
|
get language() {
|
||||||
return this.captions.language;
|
return (captions.getCurrentTrack.call(this) || {}).language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -970,119 +932,32 @@ class Plyr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the player controls
|
* Toggle the player controls
|
||||||
* @param {boolean} toggle - Whether to show the controls
|
* @param {boolean} [toggle] - Whether to show the controls
|
||||||
*/
|
*/
|
||||||
toggleControls(toggle) {
|
toggleControls(toggle) {
|
||||||
// We need controls of course...
|
// Don't toggle if missing UI support or if it's audio
|
||||||
if (!utils.is.element(this.elements.controls)) {
|
if (this.supported.ui && !this.isAudio) {
|
||||||
return;
|
// 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
|
// Negate the argument if not undefined since adding the class to hides the controls
|
||||||
if (!this.supported.ui || this.isAudio) {
|
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delay = 0;
|
// Apply and get updated state
|
||||||
let show = toggle;
|
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||||
let isEnterFullscreen = false;
|
|
||||||
|
|
||||||
// Get toggle state if not set
|
// Close menu
|
||||||
if (!utils.is.boolean(toggle)) {
|
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||||
if (utils.is.event(toggle)) {
|
controls.toggleMenu.call(this, false);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
// Trigger event on change
|
||||||
|
if (hiding !== isHidden) {
|
||||||
// Clear timer on every call
|
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||||
clearTimeout(this.timers.controls);
|
utils.dispatchEvent.call(this, this.media, eventName);
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
return !hiding;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set hideControls class
|
|
||||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1258,7 +1133,7 @@ class Plyr {
|
|||||||
} else if (utils.is.nodeList(selector)) {
|
} else if (utils.is.nodeList(selector)) {
|
||||||
targets = Array.from(selector);
|
targets = Array.from(selector);
|
||||||
} else if (utils.is.array(selector)) {
|
} else if (utils.is.array(selector)) {
|
||||||
targets = selector.filter(i => utils.is.element(i));
|
targets = selector.filter(utils.is.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.is.empty(targets)) {
|
if (utils.is.empty(targets)) {
|
||||||
@@ -1269,4 +1144,6 @@ class Plyr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Plyr.defaults = utils.cloneDeep(defaults);
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Plyr Polyfilled Build
|
// Plyr Polyfilled Build
|
||||||
// plyr.js v3.3.7
|
// plyr.js v3.3.12
|
||||||
// https://github.com/sampotts/plyr
|
// https://github.com/sampotts/plyr
|
||||||
// License: The MIT License (MIT)
|
// License: The MIT License (MIT)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
import 'custom-event-polyfill';
|
import 'custom-event-polyfill';
|
||||||
|
import 'url-polyfill';
|
||||||
import Plyr from './plyr';
|
import Plyr from './plyr';
|
||||||
|
|
||||||
export default Plyr;
|
export default Plyr;
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key) {
|
||||||
if (!Storage.supported) {
|
if (!Storage.supported || !this.enabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const support = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.addEventListener('test', null, options);
|
window.addEventListener('test', null, options);
|
||||||
|
window.removeEventListener('test', null, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-172
@@ -55,8 +55,10 @@ const ui = {
|
|||||||
// Remove native controls
|
// Remove native controls
|
||||||
ui.toggleNativeControls.call(this);
|
ui.toggleNativeControls.call(this);
|
||||||
|
|
||||||
// Captions
|
// Setup captions for HTML5
|
||||||
captions.setup.call(this);
|
if (this.isHTML5) {
|
||||||
|
captions.setup.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset volume
|
// Reset volume
|
||||||
this.volume = null;
|
this.volume = null;
|
||||||
@@ -74,10 +76,10 @@ const ui = {
|
|||||||
this.quality = null;
|
this.quality = null;
|
||||||
|
|
||||||
// Reset volume display
|
// Reset volume display
|
||||||
ui.updateVolume.call(this);
|
controls.updateVolume.call(this);
|
||||||
|
|
||||||
// Reset time display
|
// Reset time display
|
||||||
ui.timeUpdate.call(this);
|
controls.timeUpdate.call(this);
|
||||||
|
|
||||||
// Update the UI
|
// Update the UI
|
||||||
ui.checkPlaying.call(this);
|
ui.checkPlaying.call(this);
|
||||||
@@ -105,8 +107,16 @@ const ui = {
|
|||||||
// Set the title
|
// Set the title
|
||||||
ui.setTitle.call(this);
|
ui.setTitle.call(this);
|
||||||
|
|
||||||
// Set the poster image
|
// Assure the poster image is set, if the property was added before the element was created
|
||||||
ui.setPoster.call(this);
|
if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
|
||||||
|
ui.setPoster.call(this, this.poster);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually set the duration if user has overridden it.
|
||||||
|
// The event listeners for it doesn't get called if preload is disabled (#701)
|
||||||
|
if (this.config.duration) {
|
||||||
|
controls.durationUpdate.call(this);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Setup aria attribute for play and iframe title
|
// Setup aria attribute for play and iframe title
|
||||||
@@ -146,15 +156,39 @@ const ui = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set the poster image
|
// Toggle poster
|
||||||
setPoster() {
|
togglePoster(enable) {
|
||||||
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
|
utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
|
||||||
return;
|
},
|
||||||
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the inline style
|
// Load the image, and set poster if successful
|
||||||
const posters = this.poster.split(',');
|
const loadPromise = utils.loadImage(poster)
|
||||||
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
|
.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
|
// Check playing state
|
||||||
@@ -173,7 +207,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle controls
|
// Toggle controls
|
||||||
this.toggleControls(!this.playing);
|
ui.toggleControls.call(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media is loading
|
// Check if media is loading
|
||||||
@@ -188,171 +222,22 @@ const ui = {
|
|||||||
|
|
||||||
// Timer to prevent flicker when seeking
|
// Timer to prevent flicker when seeking
|
||||||
this.timers.loading = setTimeout(() => {
|
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);
|
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||||
|
|
||||||
// Show controls if loading, hide if done
|
// Update controls visibility
|
||||||
this.toggleControls(this.loading);
|
ui.toggleControls.call(this);
|
||||||
}, this.loading ? 250 : 0);
|
}, this.loading ? 250 : 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if media failed to load
|
// Toggle controls based on state and `force` argument
|
||||||
checkFailed() {
|
toggleControls(force) {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
|
const { controls } = this.elements;
|
||||||
this.failed = this.media.networkState === 3;
|
|
||||||
|
|
||||||
if (this.failed) {
|
if (controls && this.config.hideControls) {
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
|
// Show controls if force, loading, paused, or button interaction, otherwise hide
|
||||||
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
|
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 forceHours = utils.getHours(this.duration) > 0;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
target.textContent = utils.formatTime(time, forceHours, 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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+86
-53
@@ -3,73 +3,72 @@
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
import loadjs from 'loadjs';
|
import loadjs from 'loadjs';
|
||||||
|
import Storage from './storage';
|
||||||
import support from './support';
|
import support from './support';
|
||||||
import { providers } from './types';
|
import { providers } from './types';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
// Check variable types
|
// Check variable types
|
||||||
is: {
|
is: {
|
||||||
plyr(input) {
|
|
||||||
return this.instanceof(input, window.Plyr);
|
|
||||||
},
|
|
||||||
object(input) {
|
object(input) {
|
||||||
return this.getConstructor(input) === Object;
|
return utils.getConstructor(input) === Object;
|
||||||
},
|
},
|
||||||
number(input) {
|
number(input) {
|
||||||
return this.getConstructor(input) === Number && !Number.isNaN(input);
|
return utils.getConstructor(input) === Number && !Number.isNaN(input);
|
||||||
},
|
},
|
||||||
string(input) {
|
string(input) {
|
||||||
return this.getConstructor(input) === String;
|
return utils.getConstructor(input) === String;
|
||||||
},
|
},
|
||||||
boolean(input) {
|
boolean(input) {
|
||||||
return this.getConstructor(input) === Boolean;
|
return utils.getConstructor(input) === Boolean;
|
||||||
},
|
},
|
||||||
function(input) {
|
function(input) {
|
||||||
return this.getConstructor(input) === Function;
|
return utils.getConstructor(input) === Function;
|
||||||
},
|
},
|
||||||
array(input) {
|
array(input) {
|
||||||
return !this.nullOrUndefined(input) && Array.isArray(input);
|
return !utils.is.nullOrUndefined(input) && Array.isArray(input);
|
||||||
},
|
},
|
||||||
weakMap(input) {
|
weakMap(input) {
|
||||||
return this.instanceof(input, window.WeakMap);
|
return utils.is.instanceof(input, WeakMap);
|
||||||
},
|
},
|
||||||
nodeList(input) {
|
nodeList(input) {
|
||||||
return this.instanceof(input, window.NodeList);
|
return utils.is.instanceof(input, NodeList);
|
||||||
},
|
},
|
||||||
element(input) {
|
element(input) {
|
||||||
return this.instanceof(input, window.Element);
|
return utils.is.instanceof(input, Element);
|
||||||
},
|
},
|
||||||
textNode(input) {
|
textNode(input) {
|
||||||
return this.getConstructor(input) === Text;
|
return utils.getConstructor(input) === Text;
|
||||||
},
|
},
|
||||||
event(input) {
|
event(input) {
|
||||||
return this.instanceof(input, window.Event);
|
return utils.is.instanceof(input, Event);
|
||||||
},
|
},
|
||||||
cue(input) {
|
cue(input) {
|
||||||
return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
|
return utils.is.instanceof(input, window.TextTrackCue) || utils.is.instanceof(input, window.VTTCue);
|
||||||
},
|
},
|
||||||
track(input) {
|
track(input) {
|
||||||
return this.instanceof(input, TextTrack) || (!this.nullOrUndefined(input) && this.string(input.kind));
|
return utils.is.instanceof(input, TextTrack) || (!utils.is.nullOrUndefined(input) && utils.is.string(input.kind));
|
||||||
},
|
},
|
||||||
url(input) {
|
url(input) {
|
||||||
return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
|
return !utils.is.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
|
||||||
},
|
},
|
||||||
nullOrUndefined(input) {
|
nullOrUndefined(input) {
|
||||||
return input === null || typeof input === 'undefined';
|
return input === null || typeof input === 'undefined';
|
||||||
},
|
},
|
||||||
empty(input) {
|
empty(input) {
|
||||||
return (
|
return (
|
||||||
this.nullOrUndefined(input) ||
|
utils.is.nullOrUndefined(input) ||
|
||||||
((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
|
((utils.is.string(input) || utils.is.array(input) || utils.is.nodeList(input)) && !input.length) ||
|
||||||
(this.object(input) && !Object.keys(input).length)
|
(utils.is.object(input) && !Object.keys(input).length)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
instanceof(input, constructor) {
|
instanceof(input, constructor) {
|
||||||
return Boolean(input && constructor && input instanceof constructor);
|
return Boolean(input && constructor && input instanceof constructor);
|
||||||
},
|
},
|
||||||
getConstructor(input) {
|
},
|
||||||
return !this.nullOrUndefined(input) ? input.constructor : null;
|
|
||||||
},
|
getConstructor(input) {
|
||||||
|
return !utils.is.nullOrUndefined(input) ? input.constructor : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unfortunately, due to mixed support, UA sniffing is required
|
// Unfortunately, due to mixed support, UA sniffing is required
|
||||||
@@ -122,6 +121,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
|
// Load an external script
|
||||||
loadScript(url) {
|
loadScript(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -138,27 +152,28 @@ const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = 'cache-';
|
const prefix = 'cache';
|
||||||
const hasId = utils.is.string(id);
|
const hasId = utils.is.string(id);
|
||||||
let isCached = false;
|
let isCached = false;
|
||||||
|
|
||||||
const exists = () => document.querySelectorAll(`#${id}`).length;
|
const exists = () => document.getElementById(id) !== null;
|
||||||
|
|
||||||
|
const update = (container, data) => {
|
||||||
|
container.innerHTML = data;
|
||||||
|
|
||||||
function injectSprite(data) {
|
|
||||||
// Check again incase of race condition
|
// Check again incase of race condition
|
||||||
if (hasId && exists()) {
|
if (hasId && exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject content
|
|
||||||
this.innerHTML = data;
|
|
||||||
|
|
||||||
// Inject the SVG to the body
|
// Inject the SVG to the body
|
||||||
document.body.insertBefore(this, document.body.childNodes[0]);
|
document.body.insertAdjacentElement('afterbegin', container);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Only load once if ID set
|
// Only load once if ID set
|
||||||
if (!hasId || !exists()) {
|
if (!hasId || !exists()) {
|
||||||
|
const useStorage = Storage.supported;
|
||||||
|
|
||||||
// Create container
|
// Create container
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
utils.toggleHidden(container, true);
|
utils.toggleHidden(container, true);
|
||||||
@@ -168,14 +183,13 @@ const utils = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check in cache
|
// Check in cache
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
const cached = window.localStorage.getItem(prefix + id);
|
const cached = window.localStorage.getItem(`${prefix}-${id}`);
|
||||||
isCached = cached !== null;
|
isCached = cached !== null;
|
||||||
|
|
||||||
if (isCached) {
|
if (isCached) {
|
||||||
const data = JSON.parse(cached);
|
const data = JSON.parse(cached);
|
||||||
injectSprite.call(container, data.content);
|
update(container, data.content);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,16 +201,16 @@ const utils = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (support.storage) {
|
if (useStorage) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
prefix + id,
|
`${prefix}-${id}`,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
content: result,
|
content: result,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
injectSprite.call(container, result);
|
update(container, result);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -250,7 +264,7 @@ const utils = {
|
|||||||
|
|
||||||
// Add text node
|
// Add text node
|
||||||
if (utils.is.string(text)) {
|
if (utils.is.string(text)) {
|
||||||
element.textContent = text;
|
element.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return built element
|
// Return built element
|
||||||
@@ -393,14 +407,16 @@ const utils = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle class on an element
|
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||||
toggleClass(element, className, toggle) {
|
toggleClass(element, className, force) {
|
||||||
if (utils.is.element(element)) {
|
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);
|
element.classList[method](className);
|
||||||
|
return element.classList.contains(className);
|
||||||
return (toggle && !contains) || (!toggle && contains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -547,7 +563,7 @@ const utils = {
|
|||||||
const event = new CustomEvent(type, {
|
const event = new CustomEvent(type, {
|
||||||
bubbles,
|
bubbles,
|
||||||
detail: Object.assign({}, detail, {
|
detail: Object.assign({}, detail, {
|
||||||
plyr: utils.is.plyr(this) ? this : null,
|
plyr: this,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,7 +599,7 @@ const utils = {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
|
return input.toString().replace(/{(\d+)}/g, (match, i) => (utils.is.string(args[i]) ? args[i] : ''));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get percentage
|
// Get percentage
|
||||||
@@ -610,16 +626,16 @@ const utils = {
|
|||||||
formatTime(time = 0, displayHours = false, inverted = false) {
|
formatTime(time = 0, displayHours = false, inverted = false) {
|
||||||
// Bail if the value isn't a number
|
// Bail if the value isn't a number
|
||||||
if (!utils.is.number(time)) {
|
if (!utils.is.number(time)) {
|
||||||
return this.formatTime(null, displayHours, inverted);
|
return utils.formatTime(null, displayHours, inverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format time component to add leading zero
|
// Format time component to add leading zero
|
||||||
const format = value => `0${value}`.slice(-2);
|
const format = value => `0${value}`.slice(-2);
|
||||||
|
|
||||||
// Breakdown to hours, mins, secs
|
// Breakdown to hours, mins, secs
|
||||||
let hours = this.getHours(time);
|
let hours = utils.getHours(time);
|
||||||
const mins = this.getMinutes(time);
|
const mins = utils.getMinutes(time);
|
||||||
const secs = this.getSeconds(time);
|
const secs = utils.getSeconds(time);
|
||||||
|
|
||||||
// Do we need to display hours?
|
// Do we need to display hours?
|
||||||
if (displayHours || hours > 0) {
|
if (displayHours || hours > 0) {
|
||||||
@@ -706,6 +722,16 @@ const utils = {
|
|||||||
return array.filter((item, index) => array.indexOf(item) === index);
|
return array.filter((item, index) => array.indexOf(item) === index);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Clone nested objects
|
||||||
|
cloneDeep(object) {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get a nested value in an object
|
||||||
|
getDeep(object, path) {
|
||||||
|
return path.split('.').reduce((obj, key) => obj && obj[key], object);
|
||||||
|
},
|
||||||
|
|
||||||
// Get the closest value in an array
|
// Get the closest value in an array
|
||||||
closest(array, value) {
|
closest(array, value) {
|
||||||
if (!utils.is.array(array) || !array.length) {
|
if (!utils.is.array(array) || !array.length) {
|
||||||
@@ -767,10 +793,10 @@ const utils = {
|
|||||||
|
|
||||||
// Parse URL if needed
|
// Parse URL if needed
|
||||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||||
({ search } = this.parseUrl(input));
|
({ search } = utils.parseUrl(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.is.empty(search)) {
|
if (utils.is.empty(search)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,6 +832,13 @@ const utils = {
|
|||||||
return fragment.firstChild.innerText;
|
return fragment.firstChild.innerText;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Like outerHTML, but also works for DocumentFragment
|
||||||
|
getHTML(element) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.appendChild(element);
|
||||||
|
return wrapper.innerHTML;
|
||||||
|
},
|
||||||
|
|
||||||
// Get aspect ratio for dimensions
|
// Get aspect ratio for dimensions
|
||||||
getAspectRatio(width, height) {
|
getAspectRatio(width, height) {
|
||||||
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
|
const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
transition: transform 0.4s ease-in-out;
|
transition: transform 0.4s ease-in-out;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
span {
|
.plyr__caption {
|
||||||
background: $plyr-captions-bg;
|
background: $plyr-captions-bg;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
|
|||||||
@@ -18,6 +18,6 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--stopped .plyr__poster {
|
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,18 @@
|
|||||||
.plyr__progress {
|
.plyr__progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
left: $plyr-range-thumb-height / 2;
|
||||||
|
margin-right: $plyr-range-thumb-height;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
input[type='range'],
|
||||||
|
&__buffer {
|
||||||
|
margin-left: -($plyr-range-thumb-height / 2);
|
||||||
|
margin-right: -($plyr-range-thumb-height / 2);
|
||||||
|
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||||
|
width: calc(100% + #{$plyr-range-thumb-height});
|
||||||
|
}
|
||||||
|
|
||||||
input[type='range'] {
|
input[type='range'] {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -19,18 +29,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr__progress--buffer {
|
.plyr__progress__buffer {
|
||||||
-webkit-appearance: none; /* stylelint-disable-line */
|
-webkit-appearance: none; /* stylelint-disable-line */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
height: $plyr-range-track-height;
|
height: $plyr-range-track-height;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: -($plyr-range-track-height / 2) 0 0;
|
margin-top: -($plyr-range-track-height / 2);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -58,17 +67,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--video .plyr__progress--buffer {
|
.plyr--video .plyr__progress__buffer {
|
||||||
box-shadow: 0 1px 1px rgba(#000, 0.15);
|
box-shadow: 0 1px 1px rgba(#000, 0.15);
|
||||||
color: $plyr-video-progress-buffered-bg;
|
color: $plyr-video-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--audio .plyr__progress--buffer {
|
.plyr--audio .plyr__progress__buffer {
|
||||||
color: $plyr-audio-progress-buffered-bg;
|
color: $plyr-audio-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
.plyr--loading .plyr__progress--buffer {
|
.plyr--loading .plyr__progress__buffer {
|
||||||
animation: plyr-progress 1s linear infinite;
|
animation: plyr-progress 1s linear infinite;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
@@ -85,10 +94,10 @@
|
|||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--video.plyr--loading .plyr__progress--buffer {
|
.plyr--video.plyr--loading .plyr__progress__buffer {
|
||||||
background-color: $plyr-video-progress-buffered-bg;
|
background-color: $plyr-video-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr--audio.plyr--loading .plyr__progress--buffer {
|
.plyr--audio.plyr--loading .plyr__progress__buffer {
|
||||||
background-color: $plyr-audio-progress-buffered-bg;
|
background-color: $plyr-audio-progress-buffered-bg;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -31,15 +31,14 @@
|
|||||||
@import 'components/controls';
|
@import 'components/controls';
|
||||||
@import 'components/embed';
|
@import 'components/embed';
|
||||||
@import 'components/menus';
|
@import 'components/menus';
|
||||||
@import 'components/progress';
|
|
||||||
@import 'components/poster';
|
|
||||||
@import 'components/sliders';
|
@import 'components/sliders';
|
||||||
|
@import 'components/poster';
|
||||||
@import 'components/times';
|
@import 'components/times';
|
||||||
@import 'components/tooltips';
|
@import 'components/tooltips';
|
||||||
@import 'components/video';
|
@import 'components/video';
|
||||||
|
@import 'components/progress';
|
||||||
@import 'components/volume';
|
@import 'components/volume';
|
||||||
|
|
||||||
@import 'states/error';
|
|
||||||
@import 'states/fullscreen';
|
@import 'states/fullscreen';
|
||||||
|
|
||||||
@import 'plugins/ads';
|
@import 'plugins/ads';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user